Lazy loading y chunking en Vue Router

Lazy loading y chunking en Vue Router

Cuando empiezas a crear una SPA (Single page application), debes tener en cuenta que SPA no significa un único archivo JavaScript.

Normalmente utilizas Webpack para gestionar los builds de tu aplicación; por defecto, Webpack crea un solo archivo para todos los assets, incluso el CSS.

El primer paso, tal vez, sea separar los estilos de app.js en sus propios archivos CSS.

Para hacer esto, utilizaremos el plugin de Webpack MiniCssExtractPlugin, el cual configuraremos de esta manera:

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin');

module.exports = {
  plugins: [
    new MiniCssExtractPlugin({
      filename: '[name].css'
    })
  ],
  ...
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          {
            loader: MiniCssExtractPlugin.loader,
            options: {
              publicPath: '/public/path/to/',
            },
          },
          'css-loader',
          ... // Other loaders like sass-loader or postcss-loader
        ],
      }
      ...
    ]
  },

Esto obliga a Webpack a extraer el CSS en archivos separados, por ejemplo app.css.

Si utilizas vue-cli, esta es la configuración por defecto de Webpack.

Yendo un paso más allá

Para aplicaciones sencillas, es una buena idea mantener todo el código compilado en un solo archivo, ya que el navegador del cliente carga app.css la primera vez que el usuario accede a tu aplicación y lo mantiene en caché; en el siguiente acceso, el archivo se servirá desde la caché local del navegador (hasta que la caché expire).

Pero cuando tu aplicación empieza a crecer, el archivo app.js se volverá enorme, ralentizando la carga de la página. Incluso habrá partes de la aplicación que nunca se utilicen, por ejemplo “páginas” (en este contexto, piensa en las páginas como componentes de página de Vue, no páginas estáticas) prohibidas para usuarios normales.

En este caso, una buena solución es realizar un chunking de tu app.js utilizando componentes asíncronos (async components) para los componentes de página. Puedes dividir cada página en diferentes archivos que se cargarán cuando el usuario navegue a la ruta.

Esta estrategia utiliza la funcionalidad de code splitting de Webpack.

En la configuración de Vue Router simplemente haces lo siguiente:

const routes = [
  {
    path: '/',
    name: 'home',
    component: () => import('./views/HomeComponent')
  },
  ...
]

Webpack ahora creará un archivo separado para tus componentes de página. Pero de esta forma el usuario no recibe ningún feedback sobre el proceso de carga. Podríamos mejorar el router utilizando un componente de carga.

Un componente asíncrono debe proporcionar un Promise.resolve. Cuando escribes () => import('./views/HomeComponent'), implícitamente devuelves una Promise que resuelve el componente. Pero si quieres utilizar el manejo del estado de carga (handling loading state), necesitas devolver una Promise explícita.

Como esto:

import LoadingComponent from './LoadingComponent'

const routes = [
  {
    path: '/',
    name: 'home',
    component: lazyLoading(import('./views/HomeComponent'))
  },
  ...
]
function lazyLoadView (AsyncPageComponent) {
  const AsyncHandler = () => ({
    component: AsyncPageComponent,
    loading: LoadingComponent,
    ...
  })

  return Promise.resolve({
    functional: true,
    render: (h, { data, children }) => h(AsyncHandler, data, children)
  })
}

Como puedes ver, utilizamos un Promise.resolve que devuelve la función de renderizado del componente.

data y children son necesarios para pasar props, atributos y eventos al componente. Más información

Con estos cambios, cuando el usuario navega a /, primero la aplicación muestra el LoadingComponent y luego, cuando el componente está totalmente cargado, lo muestra.

Finalmente, cabe decir que puedes agrupar los componentes en el mismo chunk utilizando la siguiente notación.

import(/* webpackChunkName: "group-main" */ './HomeComponent.vue');
import(/* webpackChunkName: "group-main" */ './LoginComponent.vue');
import(/* webpackChunkName: "group-admin" */ './AdminPageComponent.vue');