Carga Diferida de Componentes en Vue.js: Mejora de Rendimiento

Clase 23 de 27Curso Avanzado de Vue.js 2

A medida que nuestra aplicación va creciendo, vamos buscando formas de mejorar su rendimiento. Tiene mucho sentido hacer Lazy Loading (o carga diferida) en las rutas, ya que solo abres una ruta por vez. Por lo tanto, no hace falta cargar todo el contenido de las rutas a la vez.

Por si acaso no tenías muy claro el concepto de Lazy Loading, podríamos decir es un patrón de diseño que consiste en retrasar la carga o inicialización de un objeto hasta el momento de su utilización.

Esta técnica ya la hemos utilizado con las rutas de nuestra app, en el router.js. Ahora vamos a ver cómo hacerlo con los componentes.

Actualmente, nuestros componentes de skills activas y pasivas se cargan de forma normal (síncrona). Lo que vamos a hacer ahora es cargar ambos componentes (habilidades activas y habilidades pasivas) con lazy loading, pero solo mostraremos uno, y podremos cambiar entre las activas y las pasivas con unos botones.

Por defecto, estaremos mostrando las habilidades activas, por lo que dicho componente se cargará inmediatamente cuando cargue la vista de /Hero. Sin embargo, el componente de habilidades pasivas no será cargado hasta que pulsemos el botón de Passive, que aún no hemos creado.

> 📗 Enlace a la documentación de Vue.js y componentes asíncronos: https://es.vuejs.org/v2/guide/components-dynamic-async.html#Componentes-asincronos

Hacer esto en Vue es muy fácil, gracias a los bundlers (como Webpack) que se encargan de separar el código y hacer que se utilice solo cuando es requerido.
Con un ejemplo en el código lo entenderás mejor.

El primer cambio que vamos a hacer es cargar los componentes de manera asíncrona. En vez de hacer el import que hacemos siempre vamos a hacer lo siguiente:

// /Hero/HeroSkills/Index.vue // import ActiveSkills from './ActiveSkills' // import PassiveSkills from './PassiveSkills' export default { name: 'HeroSkills', components: { ActiveSkills: () => import('./ActiveSkills'), PassiveSkills: () => import('./PassiveSkills') }, // ... }

EL import habitual lo comentamos o lo borramos, porque ya no lo vamos a usar. Con esto ya estamos cargando nuestros componentes con lazy loading. Míralo aquí:

pv-1

Los archivos 18.js y 19.js son nuestros 2 componentes de habilidades. Esto lo sé porque los he abierto y he revisado su contenido, pero esta no es la forma adecuada. ¿Recuerdas como lo hicimos en las rutas 🤔?
En el import pusimos un comentario y esto al generar el chunk de código iba a tener el nombre que nosotros le asignamos.
En código se vería así:

export default { components: { ActiveSkills: () => import(/* webpackChunkName: "ActiveSkills" */'./ActiveSkills'), PassiveSkills: () => import(/* webpackChunkName: "PassiveSkills" */'./PassiveSkills') } }

Si volvemos a la consola del navegador, ahora vemos esto:

pv-2

Mucho mejor así 👌.

Código completo del fichero /Hero/HeroSkills/Index.vue:

<template> <div class="skills-wrapper mt-5"> <h2 class="font-diablo">Skills</h2> <hr class="bg-white"> <ActiveSkills :skills="skills.active"/> <hr> <PassiveSkills :skills="skills.passive"/> </div> </template> <script> export default { name: 'HeroSkills', components: { ActiveSkills: () => import(/* webpackChunkName: "ActiveSkills" */ './ActiveSkills'), PassiveSkills: () => import(/* webpackChunkName: "PassiveSkills" */ './PassiveSkills') }, props: { skills: { required: true, type: Object } } } </script>

Hasta ahora lo único que hemos hecho es code splitting, es decir, trocear el código (en realidad lo hizo Webpack por nosotros: https://webpack.js.org/guides/code-splitting/). Y esto es así porque estamos mostrando los 2 componentes a la vez, por lo tanto se cargan los 2 a la vez y no hay lazy load.

Lo que puede ser mas interesante es tener 2 botones, cada uno asociado a un componente, y por defecto solo cargar las habilidades activas. Al pulsar el botón de las habilidades pasivas, cargar el contenido de este otro componente, es decir, hacer lazy loading (ahora sí). Vamos a ello:

Componentes dinámicos

Antes de poder hacer esto, necesitamos revisar los componentes dinámicos: https://es.vuejs.org/v2/guide/components.html#Componentes-dinamicos

En resumen, tenemos un nuevo tag con la propiedad is, que recibe como valor el nombre del componente que queremos cargar.

Podemos cargar varios componentes, por ejemplo:

import ComponentA from './ComponentA' import ComponentB from './ComponentB'

Y luego usarlos así:

<component is="ComponentA"></component> <component is="ComponentB"></component>

O así:

<component is="selectedComponent"></component>
computed: { selectedComponent () { return this.value === 'B' ? ComponentB : ComponentA } }

Si value es igual a B, cargamos el componente B, en caso contrario, cargamos el componente A.

Esto es lo que vamos a hacer con los componentes de habilidades, usar el tag <component> y cargar el componente que necesitemos con is. ¡Vamos a ello!

Componentes dinámicos asíncronos 🤯

Empecemos cambiando el HTML de nuestro componente /Hero/HeroSkills/Index.vue:

<template> <div class="skills-wrapper mt-5"> <h2 class="font-diablo">Skills</h2> <hr class="bg-white"> <b-nav pills small> <b-nav-item :active="!isPassiveSkillsActive" @click="changeComponent('ActiveSkills')">Active</b-nav-item> <b-nav-item :active="isPassiveSkillsActive" @click="changeComponent('PassiveSkills')">Passive</b-nav-item> </b-nav> <component :is="activeComponent" :skills="componentProps"/> <!-- <ActiveSkills :skills="skills.active"/> <hr> <PassiveSkills :skills="skills.passive"/> --> </div> </template>

Vamos a crear una nueva variable en el data para controlar que componente está activo y por ende, cuál mostrar:

data () { return { activeComponent: 'ActiveSkills' } }

Y unas computed properties:

computed: { /** * Dinamyc props for async dynamic components * @returns {String} */ // Con esto estamos generando "props" dinámicas // Si el componente es ActiveSkills pasa como props las activas, si no, las pasivas componentProps () { return this.activeComponent === 'ActiveSkills' ? this.skills.active : this.skills.passive }, // Nos dice si el componente "HabilidadesPasivas" está activo o no isPassiveSkillsActive () { return this.activeComponent === 'PassiveSkills' } }

En esta parte hacemos un breve hincapié.
Cerramos los ojos y pensamos "¡Qué maravillosas que son las computed properties!" Esta es la potencia de las propiedades computadas.

La regla que yo sigo para saber cuando las tienes que usar es: "Siempre". Abusa de las computadas y todo estará bien.

Hemos generado, a través de componentProps una prop (que pasamos de comp. padre a hijo) dinámica. Esto es necesario porque tenemos componentes dinámicos, por lo tanto las props no pueden ser estáticas como haciamos antes.

Ya por último, tenemos el método que hace que se cargue un componente u otro, cuando hacemos click:

methods: { changeComponent (component) { this.activeComponent = component } }

El código completo se vería así:

<template> <div class="skills-wrapper mt-5"> <h2 class="font-diablo">Skills</h2> <hr class="bg-white"> <b-nav pills small> <b-nav-item :active="!isPassiveSkillsActive" @click="changeComponent('ActiveSkills')">Active</b-nav-item> <b-nav-item :active="isPassiveSkillsActive" @click="changeComponent('PassiveSkills')">Passive</b-nav-item> </b-nav> <component :is="activeComponent" :skills="componentProps"/> </div> </template> <script> export default { name: 'HeroSkills', components: { // Dynamic Components ActiveSkills: () => import(/* webpackChunkName: "ActiveSkills" */'./ActiveSkills'), PassiveSkills: () => import(/* webpackChunkName: "PassiveSkills" */'./PassiveSkills') }, props: { skills: { required: true, type: Object } }, data () { return { activeComponent: 'ActiveSkills' } }, computed: { /** * Dinamyc props for dynamic components * @returns {String} */ componentProps () { return this.activeComponent === 'ActiveSkills' ? this.skills.active : this.skills.passive }, isPassiveSkillsActive () { return this.activeComponent === 'PassiveSkills' } }, methods: { changeComponent (component) { this.activeComponent = component } } } </script>

Este es el aspecto que tiene ahora nuestra app:

pv-3

Se ve en 2 columnas porque la app es responsive y así es como se ve en pantallas pequeñas. Ahora bien, si nos fijamos en la pestaña network de las herramientas para desarrolladores, vemos que solo ha cargado el componente de ActiveSkills. ¡Esto es genial!

Sin cerrar la pestaña de Network del navegador, hacemos click en el botón de Passive, veremos como se carga el otro componente.

pv-4

Hemos hecho lazy load de componentes con tan solo refactorizando un par de líneas. Ahora ya puedes aplicar este método a todos tus proyectos 👏.

Keep Alive

Sin embargo, tengo una (no tan buena) noticia que darte. Al hacer este cambio de pestañas, es decir, el cambio del componente ActiveSkills a PassiveSkills (o viceversa) múltiples veces, estamos haciendo otra vez peticiones de carga de imágenes. Mira lo que ha pasado al hacer el flujo Activas > Pasivas > Activas:

pv-5

Hemos cargado cada imagen varias veces, en este caso 3 veces. Cada vez que cambiamos de "pestaña", el componente se destruye, por lo tanto las imágenes de las habilidades se tiene que volver a cargar para ser renderizadas.
Esto no es una buena práctica, pues estaríamos haciendo peticiones innecesarias para cargar contenido que antes teníamos cargado. ¡Esto no pasaba cuando teníamos componentes síncronos!

Para ver cómo se crea y se destruye el componente con esta técnica de lazy loading, podemos poner el código que tienes a continuación en el componente de /HeroSkills/PassiveSkills.vue:

created () { console.log('CREADO!') }, destroyed () { console.log('DESTRUIDO!') }

Ahora, prueba de nuevo a alternar entre componentes de habilidades activas y pasivas. Puedes ver como salen los logs en la consola del navegador.

pv-6

No problem. Vue tiene una solución para esto y se llama Keep Alive. Es una de las features que más me gusta, para lo complejo que podría llegar ser.

> 📗 Documentación oficial de Keep Alive: https://es.vuejs.org/v2/guide/components-dynamic-async.html#keep-alive-con-Componentes-Dinamicos & https://es.vuejs.org/v2/api/#keep-alive

Esta definición, sacada de la documentación oficial, me parece que es perfecta:

> Keep Alive se utiliza principalmente para preservar el estado de los componentes o evitar la re-renderización.

Justo lo que nosotros queremos, que no se carguen de nuevo, en este caso, las imágenes.

Usarlo es tan fácil como envolver el componente que queremos mantener vivo con la etiqueta <keep-alive>:

<keep-alive> <component :is="activeComponent" :skills="componentProps"/> </keep-alive>

Por lo tanto, usamos keep-alive en nuestro componente y probamos. El HTML del /Hero/HeroSkills/Index.vue debería estar así:

<template> <div class="skills-wrapper mt-5"> <h2 class="font-diablo">Skills</h2> <hr class="bg-white"> <b-nav pills small> <b-nav-item :active="!isPassiveSkillsActive" @click="changeComponent('ActiveSkills')">Active</b-nav-item> <b-nav-item :active="isPassiveSkillsActive" @click="changeComponent('PassiveSkills')">Passive</b-nav-item> </b-nav> <keep-alive> <component :is="activeComponent" :skills="componentProps"/> </keep-alive> </div> </template>

En este tema hemos visto, Componentes Asíncronos, Componentes Dinámicos, Componentes Asíncronos Dinámicos con Props Dinámicas y Keep Alive. 😍

En el siguiente, vamos a terminar con esta vista, donde cargaremos los items (⚔️) de nuestro personaje.

Hasta ahora lo único que hemos hecho es code splitting, es decir, trocear el código (en realidad lo hizo Webpack por nosotros: https://webpack.js.org/guides/code-splitting/). Y esto es así porque estamos mostrando los 2 componentes a la vez, por lo tanto se cargan los 2 a la vez y no hay lazy load.Lo que puede ser mas interesante es tener 2 botones, cada uno asociado a un componente, y por defecto solo cargar las habilidades activas. Al pulsar el botón de las habilidades pasivas, cargar el contenido de este otro componente, es decir, hacer lazy loading (ahora sí). Vamos a ello:Componentes dinámicosAntes de poder hacer esto, necesitamos revisar los componentes dinámicos: https://es.vuejs.org/v2/guide/components.html#Componentes-dinamicosEn resumen, tenemos un nuevo tag con la propiedad is, que recibe como valor el nombre del componente que queremos cargar.Podemos cargar varios componentes, por ejemplo:import ComponentA from './ComponentA'import ComponentB from './ComponentB'Y luego usarlos así:O así:computed: { selectedComponent () { return this.value === 'B' ? ComponentB : ComponentA }}Si value es igual a B, cargamos el componente B, en caso contrario, cargamos el componente A.Esto es lo que vamos a hacer con los componentes de habilidades, usar el tag y cargar el componente que necesitemos con is. ¡Vamos a ello!Componentes dinámicos asíncronos 🤯Empecemos cambiando el HTML de nuestro componente /Hero/HeroSkills/Index.vue:Vamos a crear una nueva variable en el data para controlar que componente está activo y por ende, cuál mostrar:data () { return { activeComponent: 'ActiveSkills' }}Y unas computed properties:computed: { /** * Dinamyc props for async dynamic components * @returns {String} */ // Con esto estamos generando 'props' dinámicas // Si el componente es ActiveSkills pasa como props las activas, si no, las pasivas componentProps () { return this.activeComponent === 'ActiveSkills' ? this.skills.active : this.skills.passive }, // Nos dice si el componente 'HabilidadesPasivas' está activo o no isPassiveSkillsActive () { return this.activeComponent === 'PassiveSkills' }}En esta parte hacemos un breve hincapié.Cerramos los ojos y pensamos '¡Qué maravillosas que son las computed properties!' Esta es la potencia de las propiedades computadas.La regla que yo sigo para saber cuando las tienes que usar es: 'Siempre'. Abusa de las computadas y todo estará bien.Hemos generado, a través de componentProps una prop (que pasamos de comp. padre a hijo) dinámica. Esto es necesario porque tenemos componentes dinámicos, por lo tanto las props no pueden ser estáticas como haciamos antes.Ya por último, tenemos el método que hace que se cargue un componente u otro, cuando hacemos click:methods: { changeComponent (component) { this.activeComponent = component }}El código completo se vería así:Este es el aspecto que tiene ahora nuestra app:Se ve en 2 columnas porque la app es responsive y así es como se ve en pantallas pequeñas. Ahora bien, si nos fijamos en la pestaña network de las herramientas para desarrolladores, vemos que solo ha cargado el componente de ActiveSkills. ¡Esto es genial!Sin cerrar la pestaña de Network del navegador, hacemos click en el botón de Passive, veremos como se carga el otro componente.Hemos hecho lazy load de componentes con tan solo refactorizando un par de líneas. Ahora ya puedes aplicar este método a todos tus proyectos 👏.Keep AliveSin embargo, tengo una (no tan buena) noticia que darte. Al hacer este cambio de pestañas, es decir, el cambio del componente ActiveSkills a PassiveSkills (o viceversa) múltiples veces, estamos haciendo otra vez peticiones de carga de imágenes. Mira lo que ha pasado al hacer el flujo Activas > Pasivas > Activas:Hemos cargado cada imagen varias veces, en este caso 3 veces. Cada vez que cambiamos de 'pestaña', el componente se destruye, por lo tanto las imágenes de las habilidades se tiene que volver a cargar para ser renderizadas.Esto no es una buena práctica, pues estaríamos haciendo peticiones innecesarias para cargar contenido que antes teníamos cargado. ¡Esto no pasaba cuando teníamos componentes síncronos!Para ver cómo se crea y se destruye el componente con esta técnica de lazy loading, podemos poner el código que tienes a continuación en el componente de /HeroSkills/PassiveSkills.vue:created () { console.log('CREADO!')},destroyed () { console.log('DESTRUIDO!')}Ahora, prueba de nuevo a alternar entre componentes de habilidades activas y pasivas. Puedes ver como salen los logs en la consola del navegador.No problem. Vue tiene una solución para esto y se llama Keep Alive. Es una de las features que más me gusta, para lo complejo que podría llegar ser.> 📗 Documentación oficial de Keep Alive: https://es.vuejs.org/v2/guide/components-dynamic-async.html#keep-alive-con-Componentes-Dinamicos & https://es.vuejs.org/v2/api/#keep-aliveEsta definición, sacada de la documentación oficial, me parece que es perfecta:> Keep Alive se utiliza principalmente para preservar el estado de los componentes o evitar la re-renderización.Justo lo que nosotros queremos, que no se carguen de nuevo, en este caso, las imágenes.Usarlo es tan fácil como envolver el componente que queremos mantener vivo con la etiqueta : Por lo tanto, usamos keep-alive en nuestro componente y probamos. El HTML del /Hero/HeroSkills/Index.vue debería estar así:En este tema hemos visto, Componentes Asíncronos, Componentes Dinámicos, Componentes Asíncronos Dinámicos con Props Dinámicas y Keep Alive. 😍En el siguiente, vamos a terminar con esta vista, donde cargaremos los items (⚔️) de nuestro personaje.","url":"https://platzi.com/cursos/avanzado-vue/componentes-asincronos/","wordCount":40,"publisher":{"@type":"Organization","name":"Platzi INC"}}]}