webpack Code Splitting: separando seus módulos para uma performance incrível

O webpack é uma ferramenta incrível. Com ela podemos focar em escrever o código “de negócio” e não nos preocuparmos muito com a configuração dos builds, nos permitindo focar no que é importante. Adicionalmente, quando necessário, podemos personalizá-lo de acordo com nossas necessidades. Uma dessas personalizações nos permite melhorarmos o carregamento em geral da aplicação

O webpack é uma ferramenta incrível. Com ela podemos focar em escrever o código
“de negócio” e não nos preocuparmos muito com a configuração dos builds, nos
permitindo focar no que é importante. Adicionalmente, quando necessário, podemos
personalizá-lo de acordo com nossas necessidades.

Uma dessas personalizações nos permite melhorarmos o carregamento em geral da
aplicação, simplesmente dividindo o código. Alguns aspectos que devemos cuidar e
que podemos fazer facilmente com o webpack são:

  • Divisão de código, onde o usuário só precisa carregar o código que
    potencialmente será executado (por exemplo: não faz sentido o usuário carregar
    a feature X sendo que ele somente utiliza a feature Y)
  • Organização de módulos common/vendor, que são utilizados de forma
    compartilhada na aplicação
  • Cache de JavaScript que “muda pouco”. Por exemplo, não é necessário
    fazer o usuário carregar o React inteiro novamente quando for feita uma
    alteração no código da aplicação
  • Carregamento de múltiplos arquivos em paralelo, de forma a usufruir o máximo
    da rede e evitar deixar o usuário esperando

Os processos de minificação de código e compressão também ajudam na performance.
Junto com a divisão de código, podemos dar uma experiência melhor no
carregamento da aplicação para nossos usuários.

Importante: estaremos utilizando o webpack na versão 4.6.0 para este
artigo. As versões das libraries são irrelevantes nesse caso.

Conceitos Básicos

Para esse artigo, precisamos entender alguns conceitos básicos, de forma a nos
familirizarmos com a nomenclatura utilizada daqui pra frente.

Bundle

É o código “produzido” da nossa aplicação. Este código conterá os arquivos que
foram interpretados e, potencialmente, transformados pelo webpack. Uma aplicação
pode ter mais de um bundle.

Library

Algum módulo externo utilizado na aplicação, normalmente instalado via um
package manager (npm, yarn, etc.). Exemplos: moment, lodash.

Entry

É o arquivo que é informado ao webpack como “ponto de entrada” da aplicação. Na
prática, o webpack “olha” para esse arquivo para resolver toda a árvore de
dependências, realizando o carregamento e a compilação delas.

Build

É a execução do processo de transformação.

Aplicação Prática

Vamos para um cenário prático de forma a ilustrar como podemos explorar a
funcionalidade de Code Splitting do webpack. Imagine uma aplicação de
agendamento de viagens, onde temos dois perfis de usuário:

  1. Funcionário: pode agendar viagens para os clientes. Nesse agendamento, é
    feito um cálculo de datas utilizando a library
    moment.

  2. Administrador: acompanha detalhes das viagens agendadas em um dashboard.
    Esse dashboard utiliza a library d3 para
    exibir belos gráficos.

Ambos utilizam a library lodash
para terem algumas funções auxiliares.

Na prática, o funcionário nunca precisará carregar a library d3, assim
como o administrador nunca precisará carregar a library moment.

Estrutura do Projeto

.
├── package.json
├── package-lock.json
├── src
│   └── scripts
│       ├── administrador.js
│       ├── funcionario.js
│       └── index.js
└── webpack.config.js

Conteúdo dos Arquivos

funcionario.js

const lodash = require('lodash');
const moment = require('moment');

module.exports = () => {
    console.log('Utilizando lodash e moment para calcular datas', lodash, moment);
};

administrador.js

const lodash = require('lodash');
const d3 = require('d3');

module.exports = () => {
    console.log('Montando o dashboard com lodash e d3', lodash, d3);
};

index.js

const funcionario = require('./funcionario');
const administrador = require('./administrador');

funcionario();
administrador();

webpack.config.js

Importante: a propriedade optimization.splitChunks vem com um valor
default, porém, para fins de demonstração, ela será desabilitada por padrão,
sendo habilitada posteriormente.

const webpack = require('webpack');
const HtmlWebpackPlugin = require('html-webpack-plugin');

const path = require('path');

module.exports = {
    context: path.resolve(__dirname, 'src'),

    mode: 'production',

    entry: './scripts/index.js',

    output: {
        filename: '[name].[chunkhash].js',
    },

    plugins: [
        new HtmlWebpackPlugin(),
    ],

    optimization: {
        // Deixaremos false por enquanto para propósitos de demonstração
        splitChunks: false,
    },
};

Executando o Build

Ao executarmos esse build, podemos ver o que está acontecendo, além de
recebermos um hint do webpack. Vejamos:

Hash: ce292e499aa9d76225aa
Version: webpack 4.6.0
Time: 32007ms
Built at: 2018-04-30 16:59:34
                       Asset       Size  Chunks                    Chunk Names
main.c5d5bb71748040f842c8.js    537 KiB       0  [emitted]  [big]  main
                  index.html  201 bytes          [emitted]
Entrypoint main [big] = main.c5d5bb71748040f842c8.js
[126] ./scripts/administrador.js 168 bytes {0} [built]
[127] ./scripts/funcionario.js 189 bytes {0} [built]
[128] ../node_modules/d3/index.js + 502 modules 518 KiB {0} [built]
      |    503 modules
[129] ../node_modules/moment/locale sync ^./.*$ 2.91 KiB {0} [optional] [built]
[131] ./scripts/index.js 449 bytes {0} [built]
    + 127 hidden modules

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  main.c5d5bb71748040f842c8.js (537 KiB)

WARNING in entrypoint size limit: The following entrypoint(s) combined asset size exceeds the recommended limit (244 KiB). This can impact web performance.
Entrypoints:
  main (537 KiB)
      main.c5d5bb71748040f842c8.js


WARNING in webpack performance recommendations:
You can limit the size of your bundles by using import() or require.ensure to lazy load some parts of your application.
For more info visit https://webpack.js.org/guides/code-splitting/
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
       4 modules

Resultado do build sem nenhuma otimização

Podemos ver que o arquivo gerado main.js está com um tamanho considerável (537
KiB). O que aconteceu aqui foi que todas as dependências e código escrito
foram parar dentro desse arquivo. Não é isso o que queremos. Vamos então
otimizar esse build para melhorarmos essas questões.

Otimização 1: Divisão de Código

Um dos problemas do nosso código é que tanto o código para o Funcionário quanto
o código para o Administrador estão no mesmo arquivo. É “tentador” adicionar um
if (role === "funcionario") { const funcionario = require('./funcionario.js'); }, e if (role === "administrador") { const administrador = require('./administrador.js'); }, porém, isso não vai funcionar. O webpack
analiza o código como um todo para produzir os bundles, portanto, esse código,
mesmo com if, ainda assim conterá todo o código da aplicação.

Para resolvermos isso da maneira correta, temos que “avisar” o webpack que um
determinado módulo pode ser carregado de forma dinâmica. Podemos usar a sintaxe
import('').then(modulo => {}); para carregar o módulo. Assim,
alteramos o arquivo index.js:

const CONDICAO_FUNCIONARIO = true;
const CONDICAO_ADMINISTRADOR = true;

if (CONDICAO_FUNCIONARIO) {
    import(
        /* webpackChunkName: "funcionario" */
        './funcionario'
    ).then(funcionario => {
        funcionario.default();
    });
}

if (CONDICAO_ADMINISTRADOR) {
    import(
        /* webpackChunkName: "administrador" */
        './administrador'
    ).then(administrador => {
        administrador.default();
    });
}

Importante: O comentário /* webpackChunkName */ serve apenas para
indicarmos para o webpack o nome do chunk, não sendo uma propriedade
obrigatória, sendo utilizado por questões de organização. Os métodos importados
estarão localizados na propriedade default devido ao padrão de carregamento de
módulos executado pelo webpack.

Verificando o build, podemos ver que agora temos mais arquivos, porém, com
tamanhos menores:

Hash: eb3d6846a22cb3db7fc6
Version: webpack 4.6.0
Time: 5104ms
Built at: 2018-04-30 17:06:02
                                Asset       Size  Chunks                    Chunk Names
administrador.fd372788b906c0aef63c.js    312 KiB       0  [emitted]  [big]  administrador
  funcionario.e41d5111d4ecbe55cde7.js    293 KiB       1  [emitted]  [big]  funcionario
         main.5d7893d814fcf51dec05.js   1.97 KiB       2  [emitted]         main
                           index.html  201 bytes          [emitted]
Entrypoint main = main.5d7893d814fcf51dec05.js
  [0] ./scripts/index.js 441 bytes {2} [built]
  [1] ./scripts/administrador.js 168 bytes {0} [built]
  [2] ./scripts/funcionario.js 189 bytes {1} [built]
[130] ../node_modules/d3/index.js + 502 modules 518 KiB {0} [built]
      |    503 modules
[131] ../node_modules/moment/locale sync ^./.*$ 2.91 KiB {1} [optional] [built]
    + 127 hidden modules

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  administrador.fd372788b906c0aef63c.js (312 KiB)
  funcionario.e41d5111d4ecbe55cde7.js (293 KiB)

Resultado após fazermos o split do código

Otimização 2: Criação de common/vendor Chunks

Como podemos ver na imagem anterior, foi criado um bundle para cada um
dos arquivos que separamos (funcionario.js e administrador.js), porém,
podemos notar que o módulo lodash está duplicado nos dois arquivos. Isso
acontece pois desabilitamos a opção
optimization.splitChunks.
Podemos habilitá-la com os valores default apenas removendo a propriedade
splitChunks: false:

module.exports = {
    /* Restante do arquivo */

    optimization: {
    },

    /* Restante do arquivo */
};

Caso queiramos utilizar as opções default, a propriedade optimization pode
ser removida por completo:

module.exports = {
    /* Arquivo sem a propriedade `optimization` */
};

Com isso, podemos consultar o resultado do build novamente:

Hash: 238b9bc020cedb1be248
Version: webpack 4.6.0
Time: 7368ms
Built at: 2018-04-30 17:41:51
                                                    Asset       Size  Chunks             Chunk Names
vendors~administrador~funcionario.bc461d536e0696784589.js   69.4 KiB       0  [emitted]  vendors~administrador~funcionario
              vendors~funcionario.ba33f1b641ba56b39d74.js    221 KiB       1  [emitted]  vendors~funcionario
            vendors~administrador.b1331b1609f1429987ae.js    243 KiB       2  [emitted]  vendors~administrador
                    administrador.f49190da31cf3a0c8b4a.js  186 bytes       3  [emitted]  administrador
                      funcionario.5ea0dde0ccc5d666453f.js   3.48 KiB       4  [emitted]  funcionario
                             main.e8c3806c88c392e72bba.js   2.19 KiB       5  [emitted]  main
                                               index.html  201 bytes          [emitted]
Entrypoint main = main.e8c3806c88c392e72bba.js
  [0] ./scripts/index.js 441 bytes {5} [built]
  [1] ./scripts/administrador.js 168 bytes {3} [built]
  [2] ./scripts/funcionario.js 189 bytes {4} [built]
[129] ../node_modules/d3/index.js + 502 modules 518 KiB {2} [built]
      |    503 modules
[130] ../node_modules/moment/locale sync ^./.*$ 2.91 KiB {4} [optional] [built]
    + 127 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
       4 modules

Resultado de ativarmos o plugin para split dechunks

Nesse momento, o webpack produziu um arquivo vendor para cada um dos nossos
módulos. Nesse caso, o arquivo vendors~administrador~funcionario contém as
dependências de ambos administrador.js e funcionario.js.
vendors~administrador contém somente as dependências de administrador.js e
vendors~funcionario contém as dependências de funcionario.js.

Isso aconteceu por conta dos valores default de optimization.splitChunks:

splitChunks: {
    chunks: "async",
    minSize: 30000,
    minChunks: 1,
    maxAsyncRequests: 5,
    maxInitialRequests: 3,
    automaticNameDelimiter: '~',
    name: true,
    cacheGroups: {
        vendors: { /* Os bundles vendor foram gerados por conta dessa regra */
            test: /[\/]node_modules[\/]/,
            priority: -10
        },
        default: {
            minChunks: 2,
            priority: -20,
            reuseExistingChunk: true
        }
    }
}

Esse nível de otimização já nos traz vários ganhos de performance, porém,
dependendo do cenário, podemos ir um pouco mais além. Dificilmente mudaremos as
versões das libraries, portanto, podemos juntá-las todas em um bundle só.
Assim, o usuário baixará esse bundle uma vez e, somente quando a versão de
alguma library mudar, o usuário terá que baixar novamente. Portanto, ao invés
de fazer 3 requests para baixar as dependências, podemos fazer somente um.
Também podemos limitar para que somente sejam inclusas nesse arquivo as
libraries que tenham um tamanho acima de 50Kb. Conseguimos isso através da
seguinte configuração:

module.exports = {
    /* Restante do arquivo */

    optimization: {
        splitChunks: {
            cacheGroups: {
                test: /node_modules/,
                minSize: 50000,
                name: 'vendors',
            },
        },
    },

    /* Restante do arquivo */
};

Podemos ver que há uma redução no tamanho dos arquivos “de negócio”, e um
crescimento no arquivo de vendor. Além disso, novamente o webpack nos dá um
warning sobre o tamanho do arquivo vendor. Porém, dependendo do cenário, fazer
dessa maneira pode ser melhor. Por exemplo, se as dependências nunca vão mudar,
“pagar” o preço de um primeiro carregamento demorado pode valer a pena, visto
que dali em diante aquele arquivo provavelmente estará no cache do usuário.

Hash: 93807a9054a94919eb35
Version: webpack 4.6.0
Time: 3644ms
Built at: 2018-04-30 18:41:15
                                Asset       Size  Chunks                    Chunk Names
      vendors.76efdab6264ba70c5152.js    533 KiB       0  [emitted]  [big]  vendors
administrador.043a7ce5527f336989a2.js  186 bytes       1  [emitted]         administrador
  funcionario.559dd4868c9984b13bb8.js   3.48 KiB       2  [emitted]         funcionario
         main.bcfe1dbdb7dfe91b18a7.js   2.11 KiB       3  [emitted]         main
                           index.html  201 bytes          [emitted]
Entrypoint main = main.bcfe1dbdb7dfe91b18a7.js
  [0] ./scripts/index.js 649 bytes {3} [built]
  [1] ./scripts/administrador.js 168 bytes {1} [built]
  [2] ./scripts/funcionario.js 189 bytes {2} [built]
[129] ../node_modules/d3/index.js + 502 modules 518 KiB {0} [built]
      |    503 modules
[130] ../node_modules/moment/locale sync ^./.*$ 2.91 KiB {2} [optional] [built]
    + 127 hidden modules

WARNING in asset size limit: The following asset(s) exceed the recommended size limit (244 KiB).
This can impact web performance.
Assets:
  vendors.76efdab6264ba70c5152.js (533 KiB)
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = index.html
       4 modules

Resultado de melhorarmos o plugin para split dechunks

Finalizando

Existem várias possibilidades e vários trade offs quando se trata de
otimização de performance. Vale aprender as possibilidades que o webpack traz e
utilizá-las ao seu favor para grantir uma melhor experiências para os usuários.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *