Antes de empezar, si aún no leíste el primer post de esta serie, te invito a leerla. También aclarar que esta publicación pretende resumir y obtener lecciones de arquitectura frontend del post original del blog de ingeniería de facebook que puedes leer aquí; donde ellos cuentan su experiencia y métodos que usaron en el trabajo que realizaron para construir su nuevo sitio web.

Code-spliting para mejorar el rendimiento

Sabemos que el problema en las aplicaciones de una sola página ó spa, es el peso de la carga del empaquetado inicial, podríamos tener aplicaciones que demoren muchos segundos en terminar de cargar, sino gestionamos correctamente la carga de javascript a demanda o de manera modular.

El equipo de facebook implementó algo que llaman descarga de código incremental para entregar justo lo que se necesite, cuando se necesite. Para lo cual dividieron el código en paquetes según el orden en el cual se debería mostrar, haciendo uso de importaciones dinámicas y agregando niveles de carga de javascript.

Nivel 1

El nivel 1, es dar retroalimentación inmediata de la interfaz de usuario. Incluyendo el esqueleto y la carga mínima inicial:

Nivel 1, carga del esqueleto inicial - facebook

La sintaxis de código para la carga inicial, sería importar el modulo inicial:

Nivel 2

El nivel 2, incluye todo el javascript para representar completamente el contenido. Después de su carga, nada cambiará sin una interacción del usuario.

Nivel 2, carga todo el contenido inicial - facebook

Luego si el usuario interactúa con las opciones de la página, como abrir el menú superior derecho o interactuar con el scroll hacia abajo después del renderizado de Nivel 2, aparece un nuevo esqueleto para dar retroalimentación inmediata como antes:

Nivel 2, carga los componentes de interacción - facebook

Nivel 2, carga los componentes de interacción - facebook

Nivel 3

El nivel 3, incluye todo lo que se procesa después de la visualización inicial, por ejemplo, representa el contenido del menú superior derecho o el contenido luego de la interacción con scroll.

Nivel 3, carga el contenido basado en la interacción

El nivel 3 devuelve un contenedor basado en promesas para acceder al módulo luego de la carga de este. También devuelve las suscripciones para la actualización en vivo de los datos posteriormente.

El tamaño del JavaScript de la página de inicio se divide en tres niveles, así la carga se gestiona mejor, mejora el rendimiento y se sirve más rápido sin malograr la experiencia del usuario. Así Una página de JavaScript de 500 KB puede convertirse en 50 KB en el Nivel 1, 150 KB en el Nivel 2 y 300 KB en el Nivel 3.

A nivel sintaxis de código se vería de la siguiente manera:

// Nivel 1
import ModuleA from 'ModuleA';

// Nivel 2
importForDisplay ModuleBDeferred from 'ModuleB';

// Nivel 3
importForAfterDisplay ModuleCDeferred from 'ModuleC';
// ...
function onClick(e) {
  ModuleCDeferred.onReady(ModuleC => {
    ModuleC.log('Click happened! ', e);
  });
}

Pruebas A/B

Las pruebas A/B sirven para tomar decisiones acerca de funcionalidades o diseño experimental, implica evaluar y analizar los datos de campaña de cada experimentación para establecer la versión que tenga los mejores resultados.

Para esto es común proponer dos o más versiones diferentes de una interfaz de usuario, algunas personas verán una versión y otras personas otra versión, segmentadas de acuerdo a configuraciones regionales, clases de dispositivos, etc. Pero esto implica descargar código que no se suele usar. Para esto el equipo de facebook creó una API declarativa (definen la lógica pero no el control de flujo), donde a medida que se carga la página, el servidor puede verificar el experimento y enviar solo la versión requerida del código.

const Composer = importCond('NewComposerExperiment', {
  true: 'NewComposer',
  false: 'OldComposer',
});

Entrega de componentes solo cuando son necesarios

Mediante la composición de componentes, podemos tener componentes más pequeños y simples para crear componentes más complejos o grandes. De esta forma podríamos tener varios componentes que se usen internamente para crear diferentes resultados. Por ejemplo las publicaciones de News Feed, Fotos o Videos. Donde el renderizado cambia mucho dependiendo de los datos de entrada.

Dentro del nuevo facebook, estas dependencias se deciden en tiempo de ejecución, en función de qué datos se devuelven desde el back-end (Usando una nueva característica de Relay para expresar qué código se necesita renderizar). Si la publicación requiere una foto, expresan que necesitan el PhotoComponent para representar esa foto.

... on Post {
  ... on PhotoPost {
    @module('PhotoComponent.js')
    photo_data
  }
  ... on VideoPost {
    @module('VideoComponent.js')
    video_data
  }
}

Gestionar el presupuesto de Javascript

Los niveles y la entrega de dependencias de código condicionales vistas arriba, ayudan a entregar el código necesario para cada fase cuando se requiera, pero también se debe asegurar que el tamaño de código en cada nivel permanezca bajo control con el paso del tiempo.

Para esto el equipo de facebook, establece presupuestos de Javascript por producto, basados en objetivos de rendimiento, restricciones técnicas y consideraciones de producto a nivel de página y sub divisiones según el equipo y los límites del producto.

Gestionan la cantidad del presupuesto de javascript, la cual no se puede exceder. Para esto han creado algunas herramientas de monitoreo de código para comprender las dependencias, encontrar mejoras, mostrar alertas, tamaños históricos, cambios entre versiones, para así comprender el estado actual del código en relación a los presupuesto asignados.

Obtener los datos los antes posible

En el nuevo sitio lograron estandarizar la obtención de datos con las aplicaciones móviles y se aseguraron de que toda la búsqueda de datos pase por Relay y GraphQL para aprovechar sus características especiales de obtención de datos lo antes posible. Precargando datos en la solicitud inicial del servidor para mejorar el inicio y no esperar hasta que se descargue y ejecute todo el JavaScript antes de obtener datos del servidor.

Usando Relay, saben estáticamente qué datos necesita la página. Tan pronto como el servidor recibe la solicitud de una página, puede comenzar inmediatamente a preparar los datos necesarios y descargarlos en paralelo con el código requerido. Usaron una extensión interna de GraphQL, @stream, permitiéndoles responder a las solicitudes de datos tan pronto como estén listas en una sola operación de consulta, evitando viajes de ida y vuelta adicionales.

fragment HomepageData on User {
  newsFeed(first: 10) {
    edges @stream
  }
  ...AdditionalData
}

Mapa de ruta para una navegación rápida

Para lograr una navegación rápida, es importante solicitar lo antes posible los recursos necesarios para cargar la página de destino, de esta manera tratar de reducir los viajes de ida y vuelta al servidor. Para esto el front end necesita saber de forma anticipada que recursos se necesitaran para cada ruta. Facebook llama a esto Mapa de ruta y a cada entrada una definición de ruta.

Facebook creó un mapa de ruta y asoció una ruta con recursos que la página necesitará para mostrarse correctamente lo más rápido posible, de esta manera el cliente obtiene recursos incluso antes de hacer clic en un enlace:

Flujo de eventos para una navegación - facebook

Inician los eventos de recuperación de data, precargando al hover ó focus, y recuperando en el mousedown, para luego iniciar la interacción y la actualización de estados en el evento click. Para poder proporcionar una experiencia más fluida utilizan React.Suspense para continuar renderizando la ruta anterior hasta que la siguiente ruta se procese completamente mostrando un esqueleto de la interfaz destino.

Solitud de Mapa de ruta

Carga de código y datos en paralelo

El nuevo facebook.com realiza una gran cantidad de carga de código diferida para una ruta, como también el código de obtención de datos para esa ruta dentro de ella como vimos anteriormente. Al hacer esto terminaron con un flujo de la siguiente manera:

paralelo-1

Rutas cargadas de forma perezosa con dos viajes de ida y vuelta.

Para resolver este problema, crearon algo que llama EntryPoints, que son archivos que envuelven un punto de división de código y transforman las entradas en consultas. Estos archivos son muy pequeños y se descargan por adelantado para
cualquier punto de división. De esta manera el código y los datos se obtienen en paralelo, lo que les permite descargarlos en una sola consulta de ida y vuelta.

paralelo-2

Llegamos al final de este viaje de descubrimiento del nuevo facebook. Al igual que en el primer post dejo aquí algunas reflexiones sobre lo visto anteriormente:

  • Dividir el código para lograr un mejor rendimiento pero darle sentido y coherencia a la secuencia de división.
  • Prever la carga de código que vendrá luego de la interacción.
  • Usa los eventos relacionados al elemento de reacción para precargar datos.
  • Crear un mapa de ruta basado en pequeños archivos para lograr una navegación más rápida.
  • Minimizar los viajes de ida y vuelta al servidor a través de consultas en paralelo.
  • Las mejoras en ingeniería deben ir de la mano de las mejoras en experiencia de usuario.
  • Crear herramientas a medida que complementen las herramientas existentes cuando estas no solucionan problemas específicos.