El pasado 2 de febrero de 2017, Ann Taylor escribió el artículo 12 cosas que necesitamos ver en los maquetadores de páginas de WordPress en 2017 + Quién ya lo está haciendo bien en CodeInWP. Llevamos más de un año mejorando continuamente Page Builder Sandwich: investigamos, escuchamos a nuestros clientes, así que pensamos que Page Builder Sandwich llegaría al artículo «Quién ya lo está haciendo bien» de CodeInWP. Sin embargo, no lo logramos debido al rendimiento. 
Así que le preguntamos a Ann al respecto, y ella respondió:
…Estuve probando un poco la versión gratuita. Promete mucho: muchos módulos, una interfaz de usuario bastante intuitiva, una gran experiencia WYSIWYG, todo se arrastra fácilmente. Sin embargo, cuando tenía alrededor de 10 módulos en la página, se volvió difícil seguir trabajando con ella debido a la baja velocidad de edición…
Por lo tanto, según ella, Page Builder Sandwich tiene problemas de rendimiento. Y como siempre escuchamos las opiniones de los usuarios, nos pusimos a investigarlo.
Probando el rendimiento
Necesitábamos una forma de comprobar el rendimiento de PBS; como PBS se ejecuta principalmente en el navegador a través de Javascript, el generador de perfiles de la línea de tiempo de Chrome era la herramienta adecuada para el trabajo. El generador de perfiles de la línea de tiempo puede identificar y medir qué procesos se están llevando a cabo en una página web durante el tiempo de ejecución, lo cual es perfecto para encontrar cuellos de botella de velocidad.
Pensamos que debíamos probar primero el paso más sencillo: mover el ratón a través de múltiples elementos y filas. Sencillo, ¿verdad? Estábamos seguros de que no tendríamos problemas de rendimiento con algo tan simple como esto. Desafortunadamente, nos equivocamos.
Una vez que pulsamos el botón de grabar, movimos el ratón durante unos 2 segundos por la pantalla y luego pulsamos el botón de detener. Aquí está la línea de tiempo resultante del generador de perfiles:
¡Y miren por dónde! ¡Está lleno hasta arriba de cuellos de botella de rendimiento!
Están sucediendo muchas cosas en el gráfico, pero el problema más evidente es que hay muchas áreas moradas en él (lo cual no debería ocurrir). Además de eso, estas áreas moradas son visiblemente muy anchas y todas parecen alcanzar el pico del gráfico. También notarás que el gráfico siempre está lleno y que las áreas moradas casi nunca bajan a cero. Parece que el navegador siempre está ocupado haciendo algo y casi nunca descansa.
Pero, ¿qué significan realmente todas esas áreas moradas?
El generador de perfiles tuvo la amabilidad de indicarnos que se denominan reflows. Aquí está la definición de lo que es un reflow según el sitio de desarrolladores de Google:
Reflow es el nombre del proceso del navegador web para recalcular las posiciones y geometrías de los elementos en el documento… el reflow es una operación que bloquea al usuario en el navegador.
Por lo tanto, esto significa que mover el ratón por PBS desencadena una tonelada de reflows. Lo que es peor es que cada reflow pausa momentáneamente el navegador para que pueda terminar de hacer sus cálculos. Más reflows significan más pausas del navegador, y la experiencia se vuelve todavía más entrecortada. En términos más sencillos: muchas áreas moradas = malo.
Así que resultó que Ann tenía razón: sí tenemos un enorme cuello de botella de rendimiento, por lo que nuestro equipo se puso manos a la obra de inmediato para solucionarlo cuanto antes.
Solucionando nuestro problema de reflow y tirones
Antes de nada, necesitábamos investigar un poco para descubrir qué causa exactamente este reflow en primer lugar. Descubrimos que, al parecer, muchas cosas comunes en Javascript pueden provocar que el navegador cause un reflow.
Casi cualquier función que se pueda utilizar para obtener el ancho, alto o posición de un elemento del DOM puede desencadenar un reflow. Paul Irish escribió una extensa lista de todas las propiedades y funciones en Javascript que pueden hacer esto. La mala noticia es que Page Builder Sandwich utiliza muchas de estas propiedades y funciones, y todas ellas causan reflow:
- elem.getBoundingClientRect()
- window.getComputedStyle()
- window.scrollY
- y muchas más…
Solución n.º 1: Almacenar en caché procesos costosos de reflow
La solución más obvia es NO utilizar esas propiedades y funciones. Sin embargo, PBS depende en gran medida de ellas y no podemos prescindir de ellas.
La solución que nos queda es reducir al máximo las llamadas a esas funciones. La parte difícil fue que utilizábamos esas propiedades y funciones muchas veces en diferentes partes de PBS y, la mayoría de las veces, las utilizábamos en el mismo elemento del DOM varias veces. Para solucionar esto, creamos nuestro propio mecanismo de almacenamiento en caché para cada propiedad o función problemática.
Por ejemplo, la función getBoundingClientRect nos da las medidas actuales en píxeles de superior, izquierda, ancho, alto, derecha e inferior de un elemento del DOM. Utilizamos esta función todo el tiempo para calcular la ubicación donde dibujar los contornos de los elementos en PBS. Aquí está la función de almacenamiento en caché que creamos para esto:
// Cache: Our client rects are kept here, we can use this to clear them later.
var elemsWithBoundingRects = [];
window.pbsGetBoundingClientRect = function( element ) {
// Check if we already got the client rect before.
if ( ! element._boundingClientRect ) {
// If not, get it then store it for future use.
element._boundingClientRect = element.getBoundingClientRect();
elemsWithBoundingRects.push( element );
}
return element._boundingClientRect;
};
En lugar de llamar a getBoundingClientRect , ahora utilizamos pbsGetBoundingClientRect . Ahora, cada vez que se consulta client rect, almacenamos ese valor para referencia futura, limitando así el número de desencadenantes de reflow.
También necesitábamos una forma de borrar la caché cuando los client rects almacenados quedaran invalidados. Los valores del client rect almacenado cambian cuando el usuario se desplaza por la página o añade, cambia o elimina elementos en la pantalla. Así que creamos una función para borrar la caché que podemos llamar siempre que necesitemos valores actualizados de client rect:
var clearClientRects = function() {
var i;
for ( i = 0; i < elemsWithBoundingRects.length; i++ ) {
if ( elemsWithBoundingRects[ i ] ) {
elemsWithBoundingRects[ i ]._boundingClientRect = null;
}
}
elemsWithBoundingRects = [];
};
También aplicamos este mismo mecanismo de almacenamiento en caché para otras llamadas a funciones costosas que solemos utilizar.
Solución n.º 2: Agrupar lecturas y escrituras mediante FastDom
La implementación de la solución n.º 1 aumentó el rendimiento de PBS, pero no fue suficiente.
Buscando un poco más en Google, encontramos un artículo de Wilson Page sobre cómo evitar el «layout thrashing». Wilson afirma:
El Layout Thrashing (reflow) se produce cuando JavaScript escribe y luego lee violentamente del DOM repetidas veces, provocando reflows en el documento.
Al parecer, no basta con limitar las llamadas a propiedades y funciones específicas que provocan reflow. Cuando se realizan múltiples «lecturas» y «escrituras» sucesivas en el DOM, también se desencadena un reflow en el navegador. He aquí un escenario típico, tomado directamente del sitio de Wilson:
// Read
var h1 = element1.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
// Read (triggers reflow)
var h2 = element2.clientHeight;
// Write (invalidates layout)
element2.style.height = (h2 * 2) + 'px';
// Read (triggers reflow)
var h3 = element3.clientHeight;
// Write (invalidates layout)
element3.style.height = (h3 * 2) + 'px';
// Document reflows at end of frame
El ejemplo anterior desencadena 3 reflows porque las dimensiones de toda la página se invalidan tras una escritura. La solución propuesta para esto es reordenar el código para agrupar todas las lecturas y escrituras de modo que todo el conjunto solo desencadene 1 reflow en el navegador:
// Read
var h1 = element1.clientHeight;
var h2 = element2.clientHeight;
var h3 = element3.clientHeight;
// Write (invalidates layout)
element1.style.height = (h1 * 2) + 'px';
element2.style.height = (h2 * 2) + 'px';
element3.style.height = (h3 * 2) + 'px';
// Document reflows at end of frame
El fragmento anterior hace que solucionar el problema parezca fácil. Pero en realidad, en PBS, la lectura y escritura de elementos que provocan reflow se produce en muchos lugares diferentes y en muchos momentos distintos durante el ciclo de vida de PBS. Reorganizar miles de líneas de código Javascript no es viable.
Por suerte para nosotros, Wilson también proporcionó una solución en su artículo. Creó un pequeño script de utilidad para ayudar a agrupar esas lecturas y escrituras, llamado FastDom.
Aquí hay código de Page Builder Sandwich que utiliza FastDom y que también incluye nuestra Solución n.º 1 anterior:
// Bunch together the reads from other threads.
fastdom.measure( function() {
// Use our cached client rect.
var rect = window.pbsGetBoundingClientRect( elem );
// Bunch together the writes from other threads.
fastdom.mutate( function() {
elem.style.top = ( rect.top + window.pbsScrollY() ) + 'px';
elem.style.height = rect.height + 'px';
elem.style.left = rect.left + 'px';
elem.style.width = rect.width + 'px';
}.bind( this ) );
}.bind( this ) );
Lo que hace FastDom es agrupar todas las llamadas a fastdom.measure y todas las llamadas a fastdom.mutate y ejecutarlas de forma asíncrona, agrupando eficazmente todas las lecturas y escrituras para reducir todos los reflows en el navegador.
Solución n.º 3: Acceder a los objetos de estilo es costoso
También descubrimos, con la ayuda de jsPerf, que algunas funciones para obtener estilos eran más rápidas que otras.
En PBS, normalmente necesitamos obtener el valor actual de un estilo específico de un elemento. Por ejemplo, normalmente usamos esto para obtener el margen superior:
var styles = window.getComputedStyle( elem );
var marginTop = styles.marginTop;
Pero descubrimos que hacer esto es mucho más rápido:
var marginTop = elem.style.marginTop;
Sin embargo, elem.style.marginTop y getComputedStyle son muy diferentes. El primero solo nos proporciona el estilo en línea del elemento y no tiene en cuenta ninguna hoja de estilo; y el segundo tiene en cuenta tanto los estilos en línea como las hojas de estilo.
Es más, incluso cuando el resultado de getComputedStyle estaba en caché, seguía siendo muy lento. Como resultó ser, acceder al propio objeto de estilo es el proceso lento.
Afortunadamente para nosotros, PBS añade estilos en línea a los elementos y, dado que los estilos en línea tienen prioridad (la mayoría de las veces) sobre las hojas de estilo, habrá casos en los que obtener el valor del estilo en línea sea suficiente. Por lo tanto, si combinamos los dos métodos anteriores, podemos lograr un código de ejecución mucho más rápido:
var marginTop = elem.style.marginTop;
if ( ! marginTop ) {
var styles = window.getComputedStyle( elem );
marginTop = styles.marginTop;
}
Los resultados de las mejoras en el código
Nuestras 3 soluciones anteriores requirieron mucha investigación, pruebas e implementación real (y retrasaron el lanzamiento de nuestra función de Fuentes), pero al final todo valió la pena.
Volviendo a nuestra prueba inicial de mover el ratón por la pantalla durante 2 segundos. Aquí está la antigua línea de tiempo, fea y entrecortada:
Y así es como se ve nuestra nueva línea de tiempo después de las mejoras de rendimiento:
Como puedes ver, hay menos elementos morados en el gráfico. Y hay una diferencia notable en la cantidad de procesos que realiza el navegador. Los elementos rectangulares de la parte inferior también son más finos, lo que significa que los procesos se ejecutan más rápido que antes.
Si comparas la cantidad de áreas moradas altas y anchas en los gráficos antiguos y nuevos, ¡estamos ante un impresionante aumento del 200 % en el rendimiento! También estamos viendo mejoras de rendimiento generalizadas. ¡Nada mal en absoluto!
Conclusiones y aprendizajes
Para ser sinceros, no pensábamos que PBS tuviera problemas de rendimiento antes de que Ann lo mencionara. Principalmente porque utilizábamos máquinas enfocadas al desarrollo y los cuellos de botella de rendimiento no eran tan evidentes. Mirando hacia atrás, comprobar el rendimiento debería haber sido una de las principales prioridades. Pero al menos ahora, a partir de la versión 4.3, ya se ha solucionado. 
Para aquellos que deseen mejorar el rendimiento de su propio plugin de WordPress, especialmente si también utilizan Javascript de forma intensiva, aquí están mis conclusiones de esta experiencia:
- Limita/almacena en caché tus llamadas a propiedades/funciones que desencadenan reflow. Utiliza esto como guía: What forces layout/reflow. The comprehensive list.
- Agrupa las «lecturas» y «escrituras» de estilos del DOM. Utiliza FastDom para que lo haga por ti.
- Utiliza el generador de perfiles de la línea de tiempo de Chrome y asegúrate de tener la menor cantidad de reflows (elementos morados) posible.