Técnicas de optimización para animaciones de Rive en aplicaciones React

¿Qué es Rive?
Rive es una herramienta moderna para crear animaciones interactivas de alto rendimiento que puedes ejecutar en cualquier sitio. Es fácil de usar, funciona de maravilla con gráficos vectoriales, tiene un buen toolkit para desarrolladores y muchas funcionalidades potentes, como las state machines y la deformación de mallas. Solemos usarla para crear webs de marketing espectaculares.
Encontrarás un montón de ejemplos fascinantes en la biblioteca de la comunidad de Rive.
¿Qué podemos optimizar?
Rive asegura que todas sus animaciones ya rinden de maravilla, y es verdad: en comparación con Lottie, la diferencia de rendimiento se nota enseguida.
Así que no te voy a explicar cómo dibujar y animar en Rive. Te voy a contar cómo optimizar la carga de las animaciones de Rive para lograr una mejor experiencia de usuario.
Margen de mejora
Usar Rive en un proyecto de React es muy sencillo. Siguiendo la guía oficial, puedes hacerlo así.
import Rive from '@rive-app/react-canvas';export const Simple = () => <Rive src="https://cdn.rive.app/animations/vehicles.riv" />;
Cuando el componente se monta, Rive carga durante la inicialización una librería de runtime WASM independiente. Rive usa esta librería para controlar las animaciones e interactuar con ellas. Supongo que también es la razón de que su rendimiento sea tan superior al de alternativas como Lottie. Este módulo WASM no es pequeño: pesa 78KB.
El problema es que, en este caso, el módulo WASM y el archivo de animación .riv no empiezan a cargarse hasta que se ejecuta React, así que si tienes una animación de Rive en la sección Hero de tu página, puede que no aparezca hasta pasados 4-5 segundos si la página es bastante pesada.
Aunque uses frameworks modernos con SSG/SSR y el HTML inicial te llegue bastante rápido, luego tendrás que esperar a la hidratación de React para que se inicialice la animación de Rive.
Tras medir el tiempo de carga inicial de la página, el montaje del componente y el inicio de la animación, obtuve estas cifras en un proyecto de ejemplo:
React component mounted: ~2.5sRive animation playing: ~3.5s
Son valores medios y pueden variar según tu ubicación y tu conexión. Aun así, está claro que hay margen de mejora. Mi objetivo era reducir al mínimo la diferencia entre el montaje del componente y el inicio de la animación, y esto es lo que puedes hacer.
Aloja tú mismo el WASM
Si miras con atención la pestaña de red, verás que Rive no incluye el módulo WASM en el bundle de tu proyecto y siempre lo carga desde
https://unpkg.com/@rive-app/canvas@1.0.64/rive.wasm
Eso abre una conexión adicional que prefiero evitar, para servir todos los archivos desde el mismo CDN. Para solucionarlo, podemos usar esta API:
import riveWasmUrl from '@rive-app/canvas/rive.wasm';import { RuntimeLoader } from 'rive-react';RuntimeLoader.setWasmUrl(riveWasmUrl);
Puedes añadir este código al archivo del componente de página donde uses Rive. No funcionará a menos que añadas a la configuración de webpack un URL loader para los archivos wasm.
Así que, si usas Gatsby, haz esto:
exports.onCreateWebpackConfig = ({ loaders, actions }) => { actions.setWebpackConfig({ module: { rules: [ { test: /\.wasm$/, use: [loaders.url()], }, ], }, });};
Y esto, si usas Next.js:
module.exports = { webpack: (config, options) => { config.module.rules.push({ test: /\.wasm$/, use: ['url-loader'], }); return config; },};
Ahora va mejor, pero este no era el mayor problema.
Precarga el WASM
Lo siguiente que podemos hacer es precargar el módulo WASM con link rel="preload"
Como ya hemos adaptado webpack para cargar el WASM, precargarlo es muy sencillo. Añádelo al layout principal si usas Rive en todas partes, o inclúyelo con React Helmet en las páginas donde lo necesites.
import riveWasmUrl from '@rive-app/canvas/rive.wasm';<link rel="preload" href={riveWasmUrl} as="fetch" crossOrigin="anonymous" />;
Precarga la animación .riv
Puedes hacer lo mismo con el archivo .riv en la página donde se reproduce.
<link rel="preload" href="/path-to-rive-file/hero.riv" as="fetch" crossOrigin="anonymous" />
Resultados
Con esta solución conseguí que la diferencia entre el montaje del componente de React y el inicio de la animación de Rive fuera prácticamente nula. La diferencia restante rondaba los 30ms.
También tengo algunos trucos más que pueden ayudarte en determinadas situaciones.
Lazy loading de Rive
Si usas animaciones pequeñas por la página, es fácil cargarlas todas de antemano. Pero si tienes gráficos y animaciones complejas en Rive, pueden superar fácilmente los 20KB. Por eso te recomiendo usar la API Intersection Observer para cargarlas solo cuando sean visibles.
En el ejemplo de abajo puedes ver cómo el archivo .riv solo se carga al acercarnos a la sección con la animación de Rive
Para conseguirlo, hay que entender cómo funciona el componente de Rive para React. Según el código fuente de Rive, carga el archivo .riv al montarse el componente, lo que significa que, para evitar que se carguen archivos adicionales, tenemos que evitar que se renderice <RiveComponent>.
Después, con react-intersection-observer, podemos suscribirnos a la visibilidad de la sección con la animación.
Y, por último, envolvemos RiveComponent con <ImagePlaceholder> para evitar saltos de layout en la página.
ImagePlaceholder es una solución sencilla que usa un placeholder svg para mantener la proporción necesaria de la imagen. Como alternativa, puedes usar la propiedad CSS moderna
aspect-ratioo la clásica solución conpadding-bottom. Tienes el código fuente en la demo.
A continuación tienes una aplicación de ejemplo que muestra cómo funciona (ver demo)
import './styles.css';import { useRive } from '@rive-app/react-canvas';import { useInView } from 'react-intersection-observer';import riveBotAnimationUrl from './bot.riv';import ImagePlaceholder from './ImagePlaceholder';const ART_BOARD = 'HeroBot';const STATE_MACHINE_NAME = 'State Machine 1';export default function App() { const { RiveComponent, rive } = useRive({ src: riveBotAnimationUrl, autoplay: true, stateMachines: STATE_MACHINE_NAME, artboard: ART_BOARD, }); const [ref, isInView] = useInView({ triggerOnce: true, // rootMargin: "200px" threshold: 0.2, }); return ( <div className="App"> <section className="hero"> <div> <h1>Rive Lazy Loading Example</h1> <p>Scroll down the page and look at network tab</p> </div> </section> <section ref={ref} className="animation-container"> <ImagePlaceholder height={400} width={400}> {isInView && <RiveComponent />} </ImagePlaceholder> </section> </div> );}
Nota: el módulo WASM de Rive también se carga automáticamente la primera vez que se inicializa un RiveComponent, así que al posponer el renderizado también pospones la carga de un runtime bastante pesado.
Rive, React.lazy y Suspense
La última optimización, muy útil si no quieres mostrar animaciones de Rive en determinados dispositivos, consiste en usar Suspense de React 18 y la carga lazy de componentes. La arquitectura de esta solución es:
- Mueve el componente que carga, controla y reproduce la animación de Rive a un componente de React independiente.
- Impórtalo con React.lazy. Esto generará un bundle JS separado.
- Envuelve el componente cargado en lazy con Suspense.
const RiveAnimation = lazy(() => import('components/rive-animation'))const Section = () => {return ( {isMobile ? ( <img src="my-example.png"/> ) : ( <Suspense fallback={<Loading />}> <RiveAnimation /> </Suspense> )} )}export default Section
Con este enfoque puedes montar una demo completa de la solución en muy poco tiempo.
Componente de animación de Rive
¡Y ya está!
Resumamos cómo sacar el máximo partido a Rive con React:
- Aloja tú mismo la librería de runtime WASM. Así evitas una conexión http adicional con el servidor externo de unpkg, donde se aloja por defecto.
- Precarga los archivos .riv y WASM si tienes una animación de Rive en la sección Hero de la página. Reducirá drásticamente los tiempos de carga y ejecución.
- Crea un bundle separado para Rive con React lazy y Suspense para no cargar la librería cuando la red sea lenta o en dispositivos móviles.