revert, kill y el coste real de dejar el texto partido
Qué restaura exactamente revert, en qué se diferencia de kill, cuánto cuesta mantener miles de nodos vivos, y cómo integrar SplitText con la limpieza del proyecto.
El texto dividido es un estado transitorio del documento, y tratarlo como permanente es la decisión que convierte una animación bonita en un lastre de rendimiento. Cada carácter suelto es un nodo con estilo calculado, caja de layout y participación en cada recálculo; multiplicado por veinte titulares es una diferencia medible en cualquier interacción posterior, incluidas las que no tienen nada que ver con la animación. revert() es la operación que deshace todo eso, y saber cuándo llamarla —y cómo hacer que se llame sola— es la última pieza de este nivel.
- Explicar qué restaura
revert()y en qué se diferencia dekill(). - Estimar el coste en nodos de una división y decidir cuándo revertir.
- Encadenar la reversión al final de la animación de forma fiable.
- Integrar SplitText con
gsap.context()y congsap.matchMedia()para que la limpieza sea automática.
Qué restaura revert
revert() devuelve al elemento su innerHTML original, exactamente el que había antes de dividir. Los elementos generados desaparecen, los atributos aria que SplitText añadió se retiran, y el nodo de texto vuelve a ser un nodo de texto que el compositor de líneas puede componer normalmente.
Después de revertir, split.isSplit pasa a false, y los arrays chars, words y lines dejan de apuntar a nada útil. Si vuelves a llamar a split.split() sobre la misma instancia, se rehace la división con la configuración que le pases y los arrays se repueblan.
kill() va un paso más allá: además de revertir, destruye la instancia y retira los observadores internos que autoSplit había puesto para vigilar el ancho y la carga de fuentes. Una instancia revertida sigue viva y se puede volver a dividir; una instancia matada no.
const split = SplitText.create(".titular", { type: "chars" });
gsap.from(split.chars, {
yPercent: 110,
autoAlpha: 0,
stagger: 0.02,
duration: 0.6,
onComplete: () => split.revert(), // el texto vuelve a ser texto
});
Ese patrón —revertir en el onComplete— es el que deberías usar por defecto en cualquier revelado que ocurra una sola vez. El texto está dividido durante el segundo que dura la animación y vuelve a la normalidad después.
Existe además onRevert, una función que se ejecuta cada vez que la instancia revierte, útil para deshacer estilos propios que hubieras aplicado a los elementos generados.
Si llamas a revert() con la animación todavía en marcha, los tweens siguen apuntando a nodos que ya no están en el documento. No lanzan ningún error: escriben estilos sobre elementos desconectados y no se ve nada raro, salvo que el texto se queda en su estado final de golpe. Revierte en el onComplete, o mata la animación antes de revertir.
Cuánto cuesta no revertir
Los números ayudan a decidir. Un titular de cuarenta caracteres dividido en líneas, palabras y caracteres genera del orden de cincuenta elementos: unos cuarenta caracteres, ocho palabras y dos líneas. Con enmascarado, el doble.
Un párrafo de ciento cincuenta palabras dividido solo por caracteres genera unos ochocientos elementos. Cinco párrafos así son cuatro mil nodos añadidos al documento, y cada uno participa en el cálculo de estilo, en el árbol de layout y en el árbol de accesibilidad si no lo has ocultado.
El coste no está tanto en la memoria como en las operaciones posteriores. Cualquier cosa que provoque un recálculo de estilo en ese subárbol —un cambio de tema, una media query que cruza su umbral, un classList.toggle en un ancestro— pasa de recorrer cinco nodos a recorrer cuatro mil. Y cualquier consulta de geometría dentro de ese subárbol se vuelve proporcionalmente más cara.
// Contar el coste de tu propia pagina antes y despues
console.log("nodos totales:", document.getElementsByTagName("*").length);
Ejecuta eso antes de dividir y después. La diferencia es la factura que estás pagando por dejar el texto partido.
La conclusión práctica tiene tres reglas. Divide lo mínimo: si animas por líneas, no dividas por caracteres. Revierte en cuanto acabe la animación si no vas a repetirla. Y si la animación se puede repetir —porque está atada a un scroll reversible—, considera dividir por palabras en lugar de por caracteres, para que la estructura viva sea diez veces más pequeña.
Integrarlo con la limpieza del proyecto
En una aplicación con componentes que se montan y se desmontan, revertir en el onComplete no basta: si el componente desaparece a mitad de la animación, la instancia se queda viva con sus observadores de ancho y de fuentes.
La solución es no gestionarlo a mano. SplitText está integrado con gsap.context(), de modo que una instancia creada dentro de un contexto se revierte automáticamente cuando el contexto se revierte.
const ctx = gsap.context(() => {
const split = SplitText.create(".titular", { type: "lines", mask: "lines" });
gsap.from(split.lines, { yPercent: 100, stagger: 0.1, duration: 0.8 });
}, contenedor);
// Al desmontar el componente:
ctx.revert(); // revierte la animacion Y la division, en una sola llamada
Lo mismo ocurre dentro de gsap.matchMedia(), que internamente crea un contexto: al dejar de cumplirse la condición, la división desaparece sola.
const mm = gsap.matchMedia();
mm.add("(min-width: 768px)", () => {
const split = SplitText.create(".titular", { type: "chars" });
gsap.from(split.chars, { yPercent: 110, stagger: 0.02 });
// Sin return: el contexto revierte la division al salir de la condicion
});
Y en React, useGSAP() hace lo propio: la documentación oficial menciona explícitamente que las instancias de SplitText creadas dentro del hook se revierten al desmontar. Los tres mecanismos están desarrollados en las lecciones de contexto, matchMedia y useGSAP.
El caso del scroll reversible
Hay una combinación que produce un fallo desagradable y que conviene anticipar: una animación de texto dividido atada a un ScrollTrigger con toggleActions reversible.
Si reviertes en el onComplete, el texto vuelve a ser texto plano. Cuando el usuario sube y la instancia intenta reproducir hacia atrás, los elementos que la animación quería mover ya no existen. El texto se queda quieto y la animación no vuelve a ocurrir nunca.
La respuesta correcta depende de la política que hayas elegido. Si el revelado es de una sola vez, usa once: true en el ScrollTrigger y revierte tranquilo. Si es reversible, no reviertas al completar: divide por palabras o líneas para que el coste sea asumible y deja la división viva mientras el componente exista.
// Revelado de una sola vez: dividir, animar, revertir
SplitText.create(".titular", {
type: "words",
onSplit: (self) =>
gsap.from(self.words, {
yPercent: 110,
autoAlpha: 0,
stagger: 0.04,
duration: 0.6,
scrollTrigger: { trigger: ".titular", start: "top 80%", once: true },
onComplete: () => self.revert(),
}),
});
Hay una asimetría en cómo se enseña casi cualquier API de manipulación del DOM, y este plugin la deja al descubierto mejor que ninguno. Los tutoriales dedican el noventa por ciento del espacio a la operación constructiva —dividir, insertar, transformar— y despachan la destructiva en una línea al final, si es que la mencionan. Pero una operación que modifica una estructura compartida solo está completa cuando tiene su inversa, y la inversa es la que determina si tu código compone con el de los demás. Es exactamente el mismo contrato que un malloc con su free, un addEventListener con su removeEventListener o un subscribe con su unsubscribe, y la razón de que las tres parejas se enseñen siempre juntas es que la experiencia colectiva ya demostró qué pasa cuando no. Con SplitText la asimetría es especialmente peligrosa porque el estado dividido funciona: la página se ve bien, no hay errores en consola, nada se rompe visiblemente. Lo único que ocurre es que has dejado cuatro mil nodos donde había cinco, y esa degradación se paga en interacciones que nadie relacionará con la animación de texto de hace tres pantallas. La conclusión operativa que te llevas de este nivel entero es que la pregunta “¿cuándo se deshace esto?” debe formularse en el mismo momento en que escribes la operación que lo hace, no después. Si la respuesta es “al acabar la animación”, pon el onComplete ahora. Si es “al desmontarse el componente”, envuélvelo en un contexto ahora. Y si la respuesta honesta es “no se deshace nunca”, que a veces es legítima, al menos escríbelo en un comentario para que quien venga después sepa que fue una decisión y no un olvido.
- Mide el número de nodos del documento antes y después de dividir cinco párrafos por caracteres.
- Añade
revert()en elonCompletey comprueba que el número vuelve a su valor original. - Llama a
revert()a mitad de la animación y describe exactamente qué ves y qué no aparece en consola. - Envuelve una división en un
gsap.context()y comprueba quectx.revert()la deshace sin llamar a nada de SplitText. - Monta un revelado reversible por scroll con reversión en el
onCompletey reproduce el fallo del texto que ya no vuelve. Arréglalo cononce: true.