Animando la altura: la forma correcta

Seamos honestos. Animar la altura puede ser un gran dolor. Para mí, ha sido una batalla constante entre querer tener buenas animaciones y no estar dispuesto a pagar el enorme costo de rendimiento asociado con la altura de la animación. Ahora, ya está todo hecho.

Todo comenzó cuando estaba trabajando en mi proyecto paralelo: un creador de currículum donde puedes compartir enlaces a tu currículum que solo están activos durante un cierto período de tiempo.

Quería tener una buena animación para todas las secciones del currículum, y construí un componente de reacción que realizaba la animación. Sin embargo, pronto descubrí que este componente destruía por completo el rendimiento en dispositivos de gama baja y en algunos navegadores. Demonios, incluso mi Macbook Pro de gama alta estaba luchando por mantener fps fluidos en la animación.

La razón de esto es, por supuesto, que la animación de la propiedad de altura en CSS hace que el navegador realice costosas operaciones de diseño y pintura en cada cuadro. Hay una sección fantástica sobre el rendimiento de la representación en Google Web Fundamentals, le sugiero que la consulte.

Sin embargo, en resumen, siempre que cambie una propiedad geométrica en css, el navegador tiene que ajustar y realizar cálculos sobre cómo ese cambio afecta el diseño de la página, luego tendrá que volver a renderizar la página en un paso llamado pintar.

¿Por qué deberíamos preocuparnos siquiera por el rendimiento?

Puede resultar tentador ignorar el rendimiento. No es prudente, pero puede resultar tentador. Desde una perspectiva empresarial, ahorra mucho tiempo que, de lo contrario, podría dedicarlo a crear nuevas funciones.

Sin embargo, el rendimiento puede influir directamente en sus resultados. ¿De qué sirve crear muchas funciones si nadie las usa? Múltiples estudios realizados por Amazon y Google demuestran que esto es cierto. El rendimiento está directamente relacionado con el uso de la aplicación y los ingresos finales.

El otro lado del desempeño es igualmente importante. Nosotros, como desarrolladores, tenemos la responsabilidad de asegurarnos de que la web sea accesible para todos; hacemos esto porque es correcto. Porque Internet no es solo para ti y para mí, es para todos.

Como lo demuestra el excelente artículo de Addy Osmani, los dispositivos de gama baja tardan mucho más en analizar y ejecutar JavaScript en comparación con sus homólogos de gama alta.

Para evitar crear una división de clases en Internet, debemos ser implacables en nuestra búsqueda del rendimiento. Para mí, esto significó ser creativo y encontrar otro medio de lograr mis animaciones sin sacrificar el rendimiento.

Animar la altura de la manera correcta

Si no le importa el cómo, y solo desea ver un ejemplo en vivo, consulte los enlaces a continuación para el sitio de demostración, ejemplos y un paquete npm para reaccionar:

  • Sitio de demostración que utiliza la técnica
  • Ejemplo en vivo en vainilla JS
  • Ejemplo simple en reaccionar
  • Paquete NPM y documentación para react

La pregunta que me hice fue cómo podría evitar el costo de rendimiento en el que se incurre al animar la altura. Respuesta simple: no puedes.

En su lugar, necesitaba ser creativo con otras propiedades de CSS que no incurran en esos costos. Es decir, se transforma.

Dado que las transformaciones no tienen forma de influir en la altura. No podemos simplemente aplicar una propiedad simple a un elemento y listo. Necesitamos ser más inteligentes que eso.

La forma que usaremos para lograr una animación de alto rendimiento es en realidad simulando con transform: scaleY. La animación se realiza en varios pasos:

He aquí un ejemplo:

 ${this.markup} `
  • Primero necesitamos obtener la altura inicial del contenedor del elemento. Luego, configuramos la altura del contenedor exterior para que sea explícita a esta altura. Esto hará que cualquier contenido cambiante desborde el contenedor en lugar de expandir su padre.
  • Dentro del contenedor exterior tenemos otro div que está absolutamente posicionado para abarcar todo el ancho y alto del div. Este es nuestro trasfondo y se escalará una vez que cambiemos la transformación.
  • También disponemos de contenedor interior. El contenedor interno contiene el marcado y cambiará su altura de acuerdo con el contenido que contiene.
  • Aquí está el truco: una  vez que cambiamos un evento que cambia el marcado, tomamos la nueva altura del contenedor interno y calculamos la cantidad que el fondo necesita escalar para adaptarse a la nueva altura. Luego configuramos el fondo a scaleY por la nueva cantidad.
  • En javascript de vainilla, esto significa algunos trucos con renders duales. Una vez para obtener la altura del contenedor interior para calcular la escala. Luego, nuevamente para aplicar la escala al div de fondo para que realice la transformación.

Puede ver un ejemplo en vivo aquí en vanilla JS.

En este punto, nuestro fondo se ha escalado adecuadamente para crear la ilusión de altura. Pero, ¿qué pasa con el contenido circundante? Dado que ya no estamos ajustando el diseño, el contenido circundante no se ve afectado por los cambios.

Para hacer que el contenido circundante se mueva. Necesitamos ajustar el contenido usando transformY. El truco consiste en tomar la cantidad de contenido expandido y usar esto para mover el contenido circundante con transformaciones. Vea el ejemplo anterior.

Animando la altura en React

Como mencioné anteriormente, desarrollé este método mientras trabajaba en un proyecto personal en React. Este sitio de demostración utiliza este método exclusivamente en toda su "animación de altura". Consulte el sitio de demostración aquí.

Después de implementar esto con éxito, me tomé el tiempo para agregar este componente y algunos componentes de soporte a una pequeña biblioteca de animación que hice en React. Si está interesado, puede encontrar la información relevante aquí:

  • ver la biblioteca en NPM aquí
  • La documentación se puede encontrar aquí.

Los componentes más importantes de esta biblioteca son AnimateHeight y AnimateHeightContainer. Vamos a examinarlos:

// Inside a React component // handleAnimateHeight is called inside AnimateHeight and is passed the // transitionAmount and optionally selectedId if you pass that as a prop to // AnimateHeight. This means that you can use the transitionAmount to // transition your surrounding content.const handleAnimateHeight = (transitionAmount, selectedId) => { this.setState({ transitionAmount, selectedId }); }; // Takes a style prop, a shouldchange prop and a callback. shouldChange // determines when the AnimateHeight component should trigger, which is // whenever the prop changes. The same prop is used to control which // content to show.  {this.state.open && } {!this.state.open && } 
  • Ejemplo con contenido circundante en movimiento

Los ejemplos anteriores le muestran cómo usar AnimateHeight y activar manualmente el contenido circundante para ajustar. Pero, ¿qué pasa si tienes mucho contenido y no quieres hacer este proceso manualmente? En ese caso, puede usar AnimateHeight junto con AnimateHeightContainer.

Para usar AnimateHeightContainer, debe proporcionar a todos los elementos secundarios de nivel superior un accesorio llamado animateHeightId, que también debe pasarse a los componentes de AnimateHeight:

// Inside React Component const handleAnimateHeight = (transitionAmount, selectedId) => { this.setState({ transitionAmount, selectedId }); }; // AnimateHeight receives the transitionAmount and the active id of the AnimateHeight that fired. {this.state.open &&  {!this.state.open && }  // When AnimateHeight is triggered by state change // this content will move because the animateHeightId // is greater than the id of the AnimateHeight component above I will move I will also move 

Como puede ver en este ejemplo, AnimateHeight recibe la información que necesita para ajustar el contenido cuando el componente AnimateHeight se activa al cambiar de estado.

Una vez que esto sucede, el componente AnimateHeight activará la devolución de llamada y establecerá las propiedades en estado. Dentro de AnimateHeight se ve algo como esto (simplificado):

// Inside AnimateHeight componentDidUpdate() { if (update) { doUpdate() callback(transitionAmount, this.props.animateHeightId) } } // Equivalent to calling this function: const handleAnimateHeight = (transitionAmount, selectedId) => { this.setState({ transitionAmount, selectedId }); }; handleAnimateHeight(transitionAmount, this.props.animateHeight)

Ahora conoce la cantidad que el contenido hizo la transición en píxeles y la identificación del componente AnimateHeight que se disparó. Una vez que pase estos valores al AnimateHeightContainer, los tomará y hará la transición de los otros componentes dentro de sí mismo, siempre que configure animateHeightIds incrementales en los elementos secundarios de nivel superior.

Ejemplos más avanzados:

  • Mover contenido circundante con AnimateHeightContainer
  • Ejemplo de acordeón

NOTA: Si está utilizando este método para animar la altura y mover el contenido circundante, debe agregar la cantidad de transición a la altura de su página.

Conclusión

Es posible que haya notado en este artículo que en realidad no estamos animando la altura, y lo llamamos así. Por supuesto, tiene toda la razón. Sin embargo, creo firmemente que no importa cómo lo llamemos. Lo que importa es que logremos el efecto deseado con el menor costo de rendimiento posible.

Si bien creo que encontré una forma que es mejor que animar la propiedad de altura directamente, no pretendo haber inventado o pensado en algo que no se haya descubierto antes. Ni yo juzgo. Quizás animar la altura te funcione en tu escenario, no hay problema.

All I want is to enable and simplify effects that we all need to do, but sometimes incur costs that are difficult to bear. At the very least, I want to spark a discussion that is worth having. How can we improve the internet for everyone?