"El rendimiento es una funcionalidad" es un mantra habitual en el equipo de ingeniería de Webflow.
Así que, cuando los tiempos de carga del panel de control empezaron a aumentar hace unos meses (llegamos a ver valores P95 de hasta 16,24 segundos), empezamos a buscar formas de mejorar esta lenta experiencia para nuestros usuarios. Y nos pusimos un objetivo ambicioso: reducir los tiempos de carga inicial en al menos un 20%.
Tras investigar un poco, decidimos que el cambio más efectivo sería abandonar nuestra arquitectura SPA basada en cliente (con React Router) y pasar a una solución de renderizado del lado del servidor (elegimos NextJS por su amplio conjunto de funciones y sus actualizaciones frecuentes). Razonamos que una aplicación renderizada en el servidor nos ofrecería un HTML más completo en la solicitud inicial y reduciría el número de peticiones API desde el cliente. Además, empaquetaría los datos críticos con la carga inicial del panel, permitiendo al usuario interactuar con la página mucho antes.
Pero las grandes migraciones son complicadas. Queríamos asegurarnos de que cualquier migración cumpliera con los siguientes requisitos:
- Debíamos ser capaces de migrar las rutas de las páginas de forma incremental
- Queríamos que (la mayoría de) los desarrolladores del panel de control pudieran escribir código sin tener que conocer los detalles de esta migración
El primer requisito significaba que teníamos que dirigir el tráfico entre las dos versiones del panel de control del producto ruta por ruta, y el segundo requisito implicaba que debíamos refactorizar las API específicas del framework en componentes de orden superior que pudieran eliminarse una vez completada la migración a NextJS.
Migración de rutas individuales
Uno de los cambios recientes en la infraestructura de Webflow nos permitió trabajar a un mayor nivel de abstracción sin mucho esfuerzo de desarrollo: convertir la arquitectura monolítica de Webflow en una serie de aplicaciones más pequeñas dentro del contexto del código base de webflow.com. Con esta configuración, pudimos ejecutar la instancia de NextJS como su propia aplicación, compartiendo componentes existentes de la aplicación actual y utilizando la directiva de proxy inverso de nginx para dirigir el tráfico a la aplicación del lado del cliente o del servidor, ruta por ruta. Una plantilla inicial de nginx tenía este aspecto:
A medida que avanzaba la migración, extendimos esta regla para añadir rutas adicionales. Si una ruta empezaba a mostrar tasas de error elevadas, podíamos revertir fácilmente el cambio de configuración para desviar el tráfico de vuelta a la aplicación de React mientras solucionábamos la causa raíz.
Mantener los cambios de ruta independientes del código de la aplicación aportó claridad y evitó una bifurcación extra en el código. Los ingenieros que no estaban familiarizados con el proyecto de reestructuración pudieron, en su mayor parte, seguir trabajando en la aplicación existente sin obstáculos. En general, cuantas más bifurcaciones encuentra la gente en el código, más resultados potenciales deben mantener en su memoria. Al reducir la carga mental, hicimos que nuestros ingenieros fueran más productivos y cometieran menos errores.
Migración de componentes compartidos
Teníamos dos preocupaciones principales respecto a los componentes compartidos entre la aplicación renderizada en el servidor y la aplicación cliente existente: asegurarnos de que los componentes no intentaran acceder a las API del navegador cuando se renderizaran en el servidor, y gestionar los diferentes métodos que NextJS y React Router utilizan para acceder a los objetos del navegador. Para minimizar la complejidad, introdujimos una función de detección del lado del servidor, un nuevo contexto de React que permitía a cualquier componente determinar en qué framework se estaba ejecutando, y una serie de hooks personalizados que utilizan este nuevo contexto.
Función de detección del lado del servidor
Primero, añadimos una función de utilidad que nos evita crear errores en la aplicación cuando necesitamos ejecutar código que depende del entorno del servidor o del cliente:
Por ejemplo, tenemos un conjunto diferente de URL de recuperación de datos para las solicitudes del lado del servidor y del cliente, y usamos la función isServerSide para saber qué conjunto de URL utilizar:
También hay casos en los que queríamos utilizar el objeto window, y esto evitaría que la aplicación NextJS intentara hacerlo cuando trabaja en el servidor y provocara un error:
Contexto del framework y hooks personalizados
Un problema más espinoso es distinguir entre NextJS y la aplicación existente al recuperar los parámetros pathname y query en el cliente. Tanto React Router como NextJS tienen hooks para este tipo de datos, y esos hooks resultan fatales cuando se aplican en el contexto de aplicación incorrecto. Aprovechamos React.createContext y creamos hooks personalizados que leen el contexto y ejecutan la funcionalidad adecuada de React Router o de NextJS:
Resultados de rendimiento y próximos pasos
De las tres rutas principales que hemos tenido en producción hasta ahora, hemos visto una mejora significativa en promedio, incluyendo una disminución del 19% en los tiempos P95 para la carga inicial, acercándonos a nuestro objetivo inicial de una reducción del 20%.
Las grandes variaciones en el alcance de la mejora (por ejemplo, una página que muestra una disminución de casi el 50% y otra una del 15% en el P95) indican que aún queda trabajo por hacer para ajustar páginas individuales, pero ahora tenemos un mayor control sobre cómo se cargan los datos y componentes en el ciclo de vida del servidor y el cliente.
Continuamos con nuestro esfuerzo trasladando más páginas y sus datos a la carga en el servidor, e identificando más componentes que pueden renderizarse en el servidor en lugar de en el cliente. Los usuarios pueden esperar que las páginas migradas respondan más rápido en la carga inicial y disfruten de una experiencia más fluida, ya que su navegador se encarga de menos tareas durante el renderizado inicial.

¡Estamos contratando!
Buscamos talento en producto e ingeniería para que se una a nuestra misión de poner superpoderes de desarrollo al alcance de todos.










