Video Summary and Transcription
En esta charla, el orador discute cómo mejorar la animación en las aplicaciones de React y optimizar la animación en los navegadores. Explica el pipeline de animación del navegador y la importancia de evitar caídas de fotogramas. El orador compara las animaciones CSS y JavaScript, destacando los beneficios de usar la API requestAnimationFrame. También se discute la combinación de animaciones CSS y JavaScript utilizando la API de Web Animation. La charla concluye con consejos sobre cómo evitar cálculos redundantes y proporciona recursos adicionales para seguir aprendiendo.
1. Introducción a la animación en React
Bienvenidos a React Summit. En esta sesión, exploraremos cómo mejorar la animación en las aplicaciones de React. Soy Nikhil, un ingeniero de software en Boosman, apasionado por los sistemas de diseño, el rendimiento y React. Conéctate conmigo en GitHub y Twitter. ¡Empecemos!
Hola a todos, bienvenidos a otra sesión de la cumbre de React. Y en esta sesión hablaremos sobre cómo llevar nuestro juego de animation al siguiente nivel animando React con elegancia.
Antes de comenzar, les contaré un poco sobre mí. Soy Nikhil y soy un ingeniero de software aquí en Boosman. Me encanta hablar sobre los sistemas de diseño, el rendimiento y React en general.
Así que si también eres un entusiasta del front-end como yo, he dejado mis nombres de usuario de GitHub y Twitter aquí en las diapositivas. Siéntete libre de conectarte, sería una gran charla. Además de esto, me encanta dar charlas y también me encanta viajar, me encanta jugar deportes, especialmente cricket y tenis de mesa. He estado aprendiendo tenis de mesa especialmente durante bastante tiempo y es algo súper divertido. Así que sí, va a ser una buena charla. Me encantaría conectarme con todos ustedes, eso es todo.
2. Understanding Animation in Browsers
Una animación es todo lo que un usuario hace en una aplicación. Los navegadores crean fotogramas para analizar las interacciones del usuario. Se producen caídas de fotogramas cuando se compromete la ventana de 16,7 milisegundos del navegador. El navegador recorre el código, genera fotogramas, calcula estilos, determina el diseño y luego pinta todo en píxeles.
Muy bien, así que comencemos nuestra charla con una afirmación muy simple que es, ¿qué es exactamente una animation? La oración realmente lo responde todo. Todo lo que haces en una aplicación como usuario es básicamente una animation.
Ahora, es posible que estés pensando, ¿no son un botón flotante o propiedades como transform, etc., o un carrusel que aparece, todas estas cosas son animations y no otras cosas? Básicamente, lo que entiende el navegador es todo lo que haces como usuario, ya sea desplazarte, hacer clic en un botón o pasar el cursor sobre un botón, incluso si no muestra ninguna animation, todo es en realidad una animation para el navegador, ¿verdad?
Y si quieres visualizarlo en tu mente, es básicamente como un libro de imágenes que también mantiene el navegador. Cada vez que te desplazas o interactúas con tu sitio web, el navegador crea un conjunto de fotogramas para que puedas analizar lo que está sucediendo y los ejecuta todos juntos como un libro de imágenes, para que tengas una idea de que las cosas realmente se están moviendo.
Ahora, donde realmente radica el problema es cuando los fotogramas que tiene el navegador, si este ciclo de fotogramas se desordena en algún momento, ahí es donde ocurren los problemas o cuando el rendimiento de las animations disminuye. Ahora, si intentas entender qué es realmente una caída de fotogramas o por qué los fotogramas realmente caen cuando estamos animando.
Básicamente, se debe a que cada vez que tu navegador tiene una ventana de 16,7 milisegundos, cómo se calcula esa matemática es básicamente lo que ves en la pantalla aquí. Ahora, si quieres lograr 60 fotogramas por segundo y tienes mil milisegundos, si los divides, obtendrás aproximadamente 60, aproximadamente 16,7 milisegundos. Entonces, tu navegador realmente tiene 16,7 milisegundos para generar cada fotograma.
Ahora, lo que realmente necesita hacer durante este conjunto particular de tiempo. Bien. Tomemos un ejemplo. He escrito estas dos o tres líneas de código JavaScript aquí, que en realidad está creando, básicamente agregando una clase, que es mi caja especial a mi caja. Ahora, veamos cómo el navegador realmente recorre y analiza todo tu código. Primero, descubre, oh, ahora algo ha cambiado. Se encontró con algo de JavaScript y dice, oh, necesito generar otro fotograma. Y ahora comienza el tiempo de 16 milisegundos para el navegador.
Lo siguiente que analiza el navegador es, oh, OK, hay algún cambio de estilo que también ha estado en este archivo JavaScript, que estoy agregando algunas clases y también necesito averiguar qué hacen estas clases, qué tipo de recálculo de estilo debo hacer. Bien. Eso sería un segundo paso. En el tercer paso, el navegador descubre que, OK, he calculado los estilos, he ejecutado el JavaScript. Ahora necesito averiguar cómo renderizar todas estas cajas en la pantalla. Bien. Para esto, el navegador necesita conocer el diseño. Bien. Cuáles serán las dimensiones de cada una de las divisiones que tienes y cómo debe posicionarse en la pantalla. Bien. Esta es como esa fase de diseño donde el navegador está tratando de hacer esos cálculos.
En el cuarto paso, cuando haya terminado, intenta decidir que, OK, ahora he descubierto el diseño. Necesito pintar todo en términos de píxeles y para pintar.
3. Optimizando la Animación en los Navegadores
El navegador crea imágenes de mapa de bits de los elementos y los combina en la fase de pintura. Hay cinco fases en el pipeline: evaluación de JavaScript, cálculo de estilos, diseño, pintura y composición. Comprender el pipeline es esencial para optimizar las animaciones. La caída de fotogramas causa tartamudeo, por lo que es importante aprender técnicas para optimizar las animaciones, comenzando por evitar el recálculo de estilos utilizando opacidad en lugar de display none.
Lo que hace el navegador es crear imágenes de mapa de bits de tus elementos en múltiples capas y las muestra en la pantalla como píxeles, que es la fase de pintura. Correcto. Y al final, intenta combinar todas estas imágenes de mapa de bits que ha creado y aplica estilos encima de eso. Por ejemplo, si hay diferentes capas, la opacidad es una capa muy diferente encima de eso. ¿Necesita reordenar las capas en cada momento en que los usuarios interactúan con la aplicación? Correcto.
Estas son las cinco fases. Si quieres una idea concisa de eso, básicamente es la evaluación de JavaScript que acabamos de mencionar. En segundo lugar, el cálculo de estilos que básicamente realiza un reflujo, es decir, un reflujo en el sentido de que nuestros estilos cambian principalmente. En tercer lugar, el diseño para ser pintado y en quinto lugar, pero no menos importante, la fase de composición. Correcto. Así es como se ve realmente nuestro pipeline. Correcto.
Ahora, lo que estarás pensando es por qué necesitamos saber todo esto. Correcto. ¿Cuál es su importancia? La razón detrás de esto es que, como mencioné anteriormente, una vez que este pipeline está optimizado, solo entonces puedes obtener animaciones optimizadas, ya sea en React, en JavaScript o en cualquier otro lugar donde estés agregando animaciones. Correcto.
Porque cada vez que se caen fotogramas, tu navegador tiene que mantenerse al día con los fotogramas que se están generando. Correcto. Probablemente tenga que descartar los fotogramas que llegaron tarde y seguir coincidiendo con los fotogramas más recientes. Y así tiene que omitir algunos fotogramas y seguir adelante. Y es por eso que ves realmente un tartamudeo. Esa es la causa raíz. Así que intentemos obtener algunas técnicas sobre cómo podemos optimizarlas, básicamente. Muy bien.
Comenzando con algunas más simples, que es cómo podemos evitar la fase de recálculo de estilos Correcto. Lo que vimos. Entonces, si has visto en el pipeline, el navegador tiene que recalcular algunos estilos, ¿verdad? Entonces lo que podemos hacer es tratar de minimizar ese esfuerzo del navegador, que es lo que intentamos hacer en la fase de reflujo, que es evitar esas propiedades que realmente hacen que el navegador realice muchos cálculos. Por ejemplo, si te digo que si usas display none, ¿verdad? Eso significa que el elemento tiene que ser completamente eliminado de tu documento, ¿verdad? Lo que significa que sus dimensiones deben ser recalculadas. Y también los otros elementos que son vecinos de él, ¿cómo deben posicionarse nuevamente? En realidad, está calculando todo de nuevo, ¿verdad? En cambio, si es posible, es mejor usar opacidad en lugar de eso.
4. Técnicas Avanzadas para Evitar Reflujos
Para evitar los cálculos de reflujo, utiliza propiedades CSS que sean menos costosas en términos de pintura. También puedes utilizar CSS para indicar elementos que sean más costosos en términos de pintura y requieran recálculos. Sin embargo, utiliza esta propiedad con moderación para obtener un mejor rendimiento.
También hay una lista completa de otras propiedades que pueden ayudarte a evitar estos cálculos de reflujo. Pero este es solo un ejemplo. Podemos evitar estos reflujos utilizando propiedades CSS que sean menos costosas en términos de pintura porque la fase de composición es mucho más barata que la pintura, ¿verdad? Muy bien, pasemos al siguiente punto, que es cómo llevar esta técnica al siguiente nivel, lo cual es indicar a cualquier elemento a través de CSS que, hey, este es un elemento importante. Bueno, este es un elemento en mi aplicación que es un poco más costoso en términos de pintura. Simplemente le da una pista al navegador de que, por favor, realice algunos recálculos o simplemente cuide de este elemento en particular porque podría requerir algunos recursos tuyos y algunos cálculos de pintura pueden ser pesados aquí. Esta propiedad, nuevamente, debe ser utilizada con moderación en las aplicaciones. No se recomienda utilizarla de forma excesiva, pero definitivamente en algunos lugares te brinda beneficios en términos de rendimiento.
5. JavaScript Animations and Performance
Las animaciones de JavaScript no se pueden hacer solo con CSS. El método setInterval, aunque parece conveniente, tiene cuellos de botella de rendimiento. Se ejecuta en el hilo principal sin considerar su carga. Además, no garantiza llamadas de función consistentes al inicio de cada fotograma, lo que provoca pérdida de fotogramas. Para evitar estos problemas, el enfoque mejor es utilizar requestAnimationFrame, que garantiza animaciones más suaves.
De acuerdo, pasemos al siguiente paso, que son las animaciones de JavaScript, que hacemos mucho en nuestra vida diaria. ¿Correcto? Entonces, si te digo que si tuvieras que animar algo solo con JS, no puedes usar CSS. ¿Cuál sería tu primer pensamiento sin pensarlo? Dirías, sí, probablemente usaríamos setInterval. Yo diría, sí, está bien. Pero setInterval, aunque parece que puede resolver tu propósito, también tiene sus propios cuellos de botella de rendimiento. Veamos cómo.
Entonces, aquí verás este ejemplo donde tengo un useEffect. Y en esto, estoy tratando de calcular, tengo un setInterval en su lugar. Y si ves, lo estoy ejecutando aproximadamente durante 16 milisegundos en la parte inferior, que es aproximadamente 16 FPS que vimos en nuestra diapositiva anterior. ¿Correcto? Y estamos tratando de calcular nuestra posición de un elemento una y otra vez. Y estamos tratando de actualizar el estilo de transformación a esta posición particular que hemos calculado. Y estamos haciendo esto cada 60, 16 milisegundos para ser precisos. ¿Correcto? Ahora, ¿cuál es el problema con esto? ¿Por qué digo que no es bueno? El punto que plantea es que no garantiza. Si estás usando setInterval, obviamente se está ejecutando en tu hilo principal, lo que significa que no tiene en cuenta si tu hilo principal está ocupado o no. Cada 16 milisegundos, va a realizar estas operaciones, poniendo mucha carga allí. Y lo segundo es, si imaginas esta imagen aquí y tienes fotogramas que un navegador sabe que, oh, tengo que hacer evaluaciones, recálculo de estilos, diseño, composición, todo en esa línea de tiempo de 16 milisegundos. Porque no garantiza que tu llamada de función sea al inicio exacto de tu fotograma cuando estás listo para imprimir. A menudo sucede que a veces tu canalización se retrasa en el tiempo de tu fotograma. Y aquí es exactamente donde se producen las pérdidas de fotogramas y los problemas. Por eso setInterval no es bueno. Entonces, ¿cuál es un enfoque mejor? Es algo similar porque aquí te estás perdiendo este fotograma, ya sabes, estos vagones de fotogramas. Si ves esta diapositiva anterior, esto es en realidad como si ves este JavaScript y tu recálculo de estilos, en realidad es un vagón. Tu fotograma es en realidad como un marco de vagón. Mientras lo estás perdiendo, como mencioné, están apareciendo saltos en tus animaciones, es donde entra en juego requestAnimationFrame. Entonces, el enfoque mejor aquí se convierte en requestAnimationFrame. Ahora, la diferencia que tiene, como observarás, es el código que estamos escribiendo aquí es en realidad el mismo.
6. Request Animation Frame for Smoother Animations
La API requestAnimationFrame garantiza que las animaciones comiencen al inicio de cada fotograma cuando el navegador está listo para pintar. Esto asegura animaciones más suaves y un mejor rendimiento en comparación con el enfoque de setInterval. Usando ejemplos de código, podemos ver cómo las animaciones con requestAnimationFrame son más estables y consistentes.
La única diferencia ahora es que estamos utilizando la API requestAnimationFrame, que simplemente ejecuta de forma recursiva nuestra función animate aquí. Ahora veamos cómo mejora en realidad. De inmediato notarás la diferencia en la captura de pantalla que tienes a la derecha en comparación con la anterior. Ahora garantiza que el cálculo del pipeline de píxeles en el que está involucrado asegure que las funciones relativas comiencen al inicio cuando el navegador está listo para pintar. Además, agrega una optimización adicional: los frames de animación siempre se ejecutan cuando el navegador está listo para pintar, a diferencia de setInterval. No se ejecuta cada 16 milisegundos, sino que encuentra el momento en que el navegador tiene tiempo para pintar. Además, aplica todas estas automatizaciones, como si tuvieras una pestaña abierta, si cambias de pestaña o incluso si cierras el navegador. Intenta detener la optimización y la animación, lo que hace que sea más eficiente que el enfoque de setInterval.
Ahora veamos algo de código en acción. Tengo estos dos ejemplos de código. Uno es el ejemplo de setInterval que acabamos de ver y el otro es el ejemplo del mismo utilizando requestAnimationFrame. He abierto estos dos ejemplos de código uno al lado del otro y veamos cómo reaccionan ambos al medidor de FPS que hemos agregado aquí. Este es el ejemplo de setInterval. Si intento realizar algunos comportamientos de desplazamiento aquí, verás que hay cierta inestabilidad en los fotogramas. No siempre se mantiene estable en 120 FPS, lo cual está bien para este ejemplo, pero puede volverse más complejo a medida que tus animaciones se vuelven más complejas. En cambio, en el segundo ejemplo, aquí tenemos requestAnimationFrame en ejecución.
7. Choosing Between CSS and JavaScript Animations
La animación de solicitud garantiza una velocidad de fotogramas constante y un mejor rendimiento. CSS y JavaScript tienen ventajas y desventajas. CSS está acelerado por hardware y controlado por el navegador, mientras que JavaScript permite un control más detallado. La combinación de CSS y JavaScript utilizando la API de animación web proporciona un rendimiento similar con diferencias menos perceptibles.
Incluso si intento hacer un desplazamiento, no afecta mucho mi hilo principal o mi velocidad de fotogramas. ¿Verdad? Sabes, si lo ves, en realidad ni siquiera va a 119 FPS, se mantiene súper constante en 120, lo cual es mucho mejor. ¿Verdad? En términos de rendimiento. Aquí es donde la animación de solicitud, ya sabes, se asegura de que no te pierdas tus fotogramas como solíamos hacerlo en el enfoque de setInterval. ¿Verdad? Ahora tu cálculo de JavaScript tiene mucho tiempo para hacer estos cálculos y llegar a la línea de tiempo de 16 milisegundos. Muy bien. Esto es sobre JavaScript. Ahora, otra pregunta que puede surgir en tu mente es, ¿deberíamos usar CSS o deberíamos usar JavaScript? ¿Deberíamos usar ambos o ninguno? O como ninguno? No sería una opción. Algo así. ¿Deberíamos usar alguno de ellos? Ahora, ambos tienen sus ventajas y desventajas. ¿Verdad? Entonces, si piensas en CSS, está acelerado por hardware. Se ejecuta en un hilo separado que se llama hilo del compositor. Lo que significa es que puede usar otro hilo además del hilo principal, que utiliza otros recursos como la GPU, que no bloquean tu hilo principal o tu CPU. ¿Verdad? Lo cual es mucho más eficiente. Y también está controlado por el navegador. ¿Verdad? Porque tu navegador no puede decidir qué tipo de optimizaciones fuera de la caja puede agregar. Entonces CSS tiene esas ventajas, pero algunas desventajas que a veces la gente encuentra es que hay algunas animaciones más complejas que tal vez los productos del mundo real requieren, es donde se requiere un control más detallado, es donde las animaciones de JavaScript resultan ser la elección de las personas.
Ahora, ¿qué tal si combinas ambos? Entonces, como puedes usar otra API que ahora expone JavaScript, que es la API de animación web. Entonces, si ves a la izquierda y a la derecha, CSS está usando keyframes, mientras que puedes escribir este mismo code exacto utilizando la función animate que ves a la derecha usando la API de animación web. Y puedes pasar estos mismos estilos que has estado dando a los keyframes como una matriz de objetos a tu API de animación web. Puedes configurarlo y en realidad te da un rendimiento similar si hablamos de, no te da tanta interrupción. Así que es algo similar.
8. Combining CSS and JavaScript for Animations
Las animaciones CSS se pueden combinar con JavaScript utilizando la API de animación web. Hay bibliotecas disponibles para animaciones optimizadas, como Frame of Motion, Motion One y React Spring. La eficiencia consiste en hacer las cosas correctamente, mientras que la efectividad consiste en hacer las cosas correctas. La API de observador de intersección es un enfoque mejor para verificar si un elemento está en la ventana gráfica en comparación con el uso de la función getBoundingClientRect.
CSS sigue demostrando ser un poco mejor, pero es muy poco perceptible, diría yo. Para ver esto en acción nuevamente. Veamos. Entonces esta es tu CSS animation utilizando la función animate. Esto es, lo siento, tus funciones de animación, API de animación web. Y otro ejemplo que tengo es utilizando otra JavaScript animation que es nuestro antiguo set interval. Entonces, si vuelvo a abrirlos uno al lado del otro, abro también este, y trato de ver cómo responden estos abriendo de nuevo el medidor de FPS. Entonces verás de nuevo, tienes 120 FPS aquí. Y de manera similar, si abro de nuevo el medidor de FPS aquí, de nuevo tienes como 120, como aquí lo hace. Pero esto es porque este es un ejemplo de JavaScript. Así que en realidad es mucho peor que usar el mismo código de JavaScript, pero esta vez usando una API de animación web para hacer cosas. Así que en realidad te da la sensación como si estuvieras usando solo CSS, ¿verdad? Por eso puedes elegir entre si quieres usar JavaScript o CSS, o puedes combinar ambos usando la animación web, ¿verdad? Esta será tu opción.
Ahora, esto también se puede llevar a un nivel muy diferente donde si no quieres meterte en todas estas animations, ya hay un conjunto de bibliotecas que han sido probadas en batalla contra todos estos escenarios salvajes. Y te brindan todas estas optimizaciones de performance de manera predeterminada. Puedes probar Frame of Motion, puedes probar Motion One, o puedes probar React Spring, ya hay cientos de bibliotecas que ya están disponibles. Y también son muy decorativas, muy simples de usar. Ya he compartido este ejemplo aquí, donde puedes tener, por ejemplo, para un día, tienes motion o Dave, puedes darle tus secuencias de animation, puedes configurarlo . Y también es muy simple de hacer. Muy bien, ahora, con la eficiencia, también viene la efectividad, ¿verdad? Esta es una cita muy famosa de Pete Drucker que vi. Y es muy significativa para lo que estamos discutiendo aquí. Como dice, la eficiencia consiste en hacer las cosas correctamente. Y la efectividad consiste en hacer las cosas correctas. Entonces, ¿cómo podemos asegurarnos de hacer las cosas correctas y con cosas correctas me refiero a ¿podemos reducir algunas cosas redundantes en nuestras animations, verdad? Así que intentemos pensar más en eso.
Ahora hay otra batalla entre la función getBoundingClientRect y la API de observador de intersección. Muchas veces, si no estás utilizando ninguna otra API de observador de intersección, y tienes una animation que en realidad depende de asegurarse de si tu elemento está disponible en la ventana gráfica o no, es posible que estés utilizando la función getBoundingClientRect y tratando de averiguar mediante todo tipo de cálculos si está ahí o no, si está ahí o no mediante tantos eventos listeners, esto puede ser mucho más desordenado y también en términos de performance no es bueno, ¿verdad? Ahí es donde entra en juego la API de observador de intersección. Como puedes ver a la derecha, tengo este useEffect. Y simplemente te da la entrada. Está intersectando la clave aquí, con la que puedes decir fácilmente si tu elemento ya está ahí en la ventana gráfica o no. Y puedes hacer algunas cosas con eso. Entonces, en términos de implementación y en términos de performance, es un enfoque mucho mejor para hacerlo.
9. Evitando Cálculos Redundantes y Recapitulación
Evita recálculos redundantes almacenando valores como variables. Implementa rebotes y throttling para animaciones más suaves. Recapitulación de los temas discutidos: comprensión del pipeline de píxeles, optimización de estilos CSS y animaciones JavaScript, combinación de CSS y API web. Se proporcionan recursos adicionales y enlaces de demostración para seguir aprendiendo.
En la segunda fase de evitar cosas redundantes es cómo podemos tratar de evitar recálculos siempre que sea necesario o cuando podamos hacerlo. Por ejemplo, si tienes algunos elementos y aquí, si ves, estoy tratando de agregar un estilo de transformación y aquí, si ves esta línea container dot get bounding client, se ejecutará para cada elemento dentro del bucle, ¿verdad? Lo cual no va a cambiar tanto para cada elemento si ves aquí, lo cual puedes sacar del bucle. Y cada vez podemos referirnos a él como una variable y no calcularlo de nuevo una y otra vez, lo cual es una cosa muy pequeña, pero a veces resulta muy efectiva. Muy bien. Y una de las otras cosas es, como ya sabes, lo hemos estado haciendo mucho, pero esto también se aplica a las animaciones también, que son los rebotes y el throttling, ¿verdad? Entonces, si tienes una animación, depende del desplazamiento y también puedes intentar debounciar tu animación de tal manera que solo puedas o, ya sabes, limitar eso en un intervalo de tiempo específico y no hacer cientos de miles de llamadas veces, lo cual puede que ni siquiera sea necesario. Entonces puedes producir llamadas de función en esos casos. Muy bien. Con esto, es posible que estés pensando que sí, ahora somos gurús de las animaciones, y ahora sabemos mucho más sobre cómo hacer que las animaciones sean justas. Pero obviamente piensas que leer todo el tiempo no es suficiente. También hay muchas cosas por ahí que aprendes con la práctica, revisando el código y adquiriendo experiencia. Así que también hay muchas cosas que deben ser cubiertas. Pero aquí agregué un resumen rápido de lo que hemos discutido solo para asegurarnos de que hemos aprendido todo y para que tengas una idea rápida de lo que hicimos. Entonces, primero, creo que hablamos de qué es el pipeline de píxeles y cómo se refiere a las tasas de fotogramas y cómo arruinar este pipeline de píxeles va a arruinar tus tasas de fotogramas y eventualmente tus animaciones. Muy bien. Y lo siguiente es cómo podemos tratar de reducir el trabajo del navegador utilizando estilos CSS más ligeros como la opacidad y cosas así, que no son computacionalmente pesados, cómo podemos optimizar las animaciones de JavaScript utilizando los cuadros de animación de solicitud y no usar tanto el set interval. Y cómo podemos usar las animaciones de CSS más las API web y cómo podemos combinarlas ambas juntas y cómo podemos cambiar cosas así. Muy bien. Eso es lo que discutimos. Aquí hay algunos que leí mientras también me preparaba para la charla. Así que contienen mucha más información sobre cómo hacer que las animaciones sean más eficientes. Así que no olvides echarles un vistazo también y leerlos. Creo que después de esta presentación, será una lectura más rápida para que comprendas lo que está sucediendo. Y también adjunto algunos enlaces de demostración que acabamos de ver en nuestra charla. Así que podemos revisar el código y consultarlo mientras aprendemos. Muy bien. Con esto, me gustaría terminar mi charla aquí. Gracias nuevamente al equipo aquí y a la React Summit por tenerme y también a ustedes por ser oyentes tan pacientes. Espero que hayan aprendido algo valioso hoy. Así que gracias por tenerme. ¡Saludos!
Comments