Code splitting com Vue.js e Webpack

Se pararmos para ver como fazemos aplicações hoje me dia acabamos notando que separamos o nosso código front-end (me refiro ao javascript nesse caso) em varios arquivos o que facilita a manutenção, e com ajuda de ferramentas como Webpack e Browserify e até mesmo o gulp-concat, fazemos a união desses arquivos para ficar todos em um só, porque entendemos que varios requests para buscar vários arquivos era ruim e deixava lento o carregamento da página.

Mas a chegada do HTTP 2 com funcionalidades como Server push perdemos a necessidade de fazer coisas como a união do código js em um arquivo main só e outras coisas que eram levadas em conta antes com o HTTP 1.1.

Com a união de varios arquivos em um só, ainda mais com a complexidade e a quantidades de libs js que usamos temos o problema:

foto do networking do chrome mostrando o tamanho do arquivo js

O problema agora é que o usuário vai baixar o arquivo js para todas as páginas da SPA de uma vez, mesmo sem ter acessado a página antes.

Code splitting é a ideia de quebrar esse arquivo main em varios arquivos permitindo o usuário baixar somente o que ele precisa

Por exemplo se olharmos para essa app assim, podemos ver o que não era preciso carregar logo de primeira.

mostrando partes da página que não precisavam ser carregadas logo de primeira

A grande questão é, e se atrasarmos o carregamento dessas partes até depois do render inicial? o usuário conseguiria uma interação muito mais rápida.

Async components

A chave para fazer code splitting no Vue são os async components. Estes caras são os componentes que por sua definição são carregados assincronamente.

Vamos declarar um componente usando o component API (Vue.component(name, definition)). Ao invés de termos um objeto como segundo argumento, os async components tem uma função. Esta função tem duas notáveis features:

  1. É meio que um wrapper de uma Promise, tal que tem o argumento resolve.
  2. É uma factory function, tal que retorna um objeto, só que neste caso a definição do componente.
Vue.component('async-component', (resolve) => {  
  resolve({
    template: '<div>Async Component</div>',
    props: [ 'myprop' ]
  });
});

Esses async components são apenas o primeiro step para o code splitting porquê agora temos que implementar o mecanismo para haver a troca de código da nossa app.

Dynamic module loading

Nós também precisamos de uma ajuda do brother webpack. Vamos separar nosso componente em um modulo ES6:

AsyncComponent.js

export default {  
  template: '<div>Async Component</div>',
  props: [ 'myprop' ]
}

Como a gente poderia carregar isso assincronamente? ficariamos tentados a fazer isso:

import AsyncComponent from './AsyncComponent.js'`;  
Vue.component('async-component', AsyncComponent);  

Porém isso é estático e é resolvido em tempo de execução. O que precisamos é uma maneira de carregar isso durante a app rodando se quisermos ter os benefícios do code splitting.

import()

Atualmente não é possível carregar dinamicamente o arquivo de modulo com javascript, Existe, entretanto, uma função que consegue fazer isso para nós, ainda em proposal para o ECMAScript chamada import().

O Webpack já tem uma implementação para o import() que lida com o code splitting, colocando o modulo de request separado quando o bundle é criado (um chunk na verdade, mas pense como um arquivo separado por hora).

import() recebe como argumento o nome do arquivo e retorna uma Promise. Aqui esta como carregamos o modulo acima:

main.js

import(/* webpackChunkName: "async-component" */ './AsyncComponent.js')  
  .then((AsyncComponent) => {
    console.log(AsyncComponent.default.template);
    // Output: <div>Async Component</div>
  });

Caso você esteja usando o Babel, você vai precisar adicionar o plugin syntax-dynamic-import para ele conseguir parsear a sintaxe.

Agora quando você der build no projeto irá receber a notícia que um modulo apareceu como arquivo próprio:

agora existem dois arquivos sendo carregados

Dynamic component loading

Tendo em mente que o import() retorna uma Promise, podemos unir isso com a feature async component do Vue. O Webpack vai fazer o bundle do async component separadamente e injetar via AJAX quando o componente for necessário.

main.js

import Vue from 'vue';

Vue.component('async-component', (resolve) => {  
  import('./AsyncComponent.js')
    .then((AsyncComponent) => {
      resolve(AsyncComponent.default);
    });
});

new Vue({  
  el: '#app' 
});

index.html

<div id="app">  
  <p>This part is included in the page load</p>
  <async-component></async-component>
</div>  
<script src="bundle.main.js"></script>  

Quando o main.js rodar, irá acontecer um request automaticamente para o componente, isso porquê a implementação do import() para o Webpack vai carregar o module via AJAX!

Caso o AJAX chamado retorna sucesso e o modulo retornado, a Promise se resolve e o componente pode ser renderizado, então o Vue vai renderizar a página de novo:

<div id="app">  
  <p>This part is included in the page load</p>
  <div>Async Component</div>
</div>  

Aqui esta um diagrama que pode ajudar você a entender:

resultado final no html

Single file components

Beleza, mas como eu uso o code splitting com os Single file components?
É incrível como fica ainda mais simples!

AsyncComponent.vue

<template>  
  <div>Async Component</div>
</template>

<script>  
  export default {
    props: [ 'myprop' ]
  }
</script>  

E a sintaxe fica ainda mais idiota...

new Vue({  
  el: '#app',
  components: {
    AsyncComponent: () => import('./AsyncComponent.vue')
  }
});

Conclusão

Beem.... legal, sabemos o que é code splitting, sabemos como fazer no Vue com Webpack usando de duas formas, tanto com o Vue.component quando com os Single File Component, mas como eu faço uma arquitetura de uma aplicação pensando nisso?

Acho que a forma mas óbvia de pensar é separando por página, por exemplo, ter bundlers separados para uma página home e para uma página about.

Mas existem outras formas, como fazer o code splitting de qualquer componente que não é necessariamente mostrado na hora como por exemplo, tabs, modals, dropdown, menus, etc...

Então é isso pessoal, e vamos discutir o assunto nos comentários!

Referências