Diminuir o tamanho do front-end

Como usar o Webpack para deixar seu app o menor possível

Uma das primeiras coisas a fazer ao otimizar um aplicativo é torná-lo o menor possível. Veja como fazer isso com o webpack.

Usar o modo de produção (somente webpack 4)

O Webpack 4 introduziu a nova flag mode. É possível definir essa flag como 'development' ou 'production' para indicar ao webpack que você está criando o aplicativo para um ambiente específico:

// webpack.config.js
module.exports = {
  mode: 'production',
};

Ative o modo production ao criar o app para produção. Isso fará com que o Webpack aplique otimizações, como minificação, remoção de código somente para desenvolvimento em bibliotecas e muito mais.

Leitura adicional

Ativar minificação

A minificação é quando você compacta o código removendo espaços extras, encurtando nomes de variáveis e assim por diante. Assim:

// Original code
function map(array, iteratee) {
  let index = -1;
  const length = array == null ? 0 : array.length;
  const result = new Array(length);

  while (++index < length) {
    result[index] = iteratee(array[index], index, array);
  }
  return result;
}

// Minified code
function map(n,r){let t=-1;for(const a=null==n?0:n.length,l=Array(a);++t<a;)l[t]=r(n[t],t,n);return l}

O Webpack oferece suporte a duas maneiras de minimizar o código: a minimização no nível do pacote e opções específicas do loader. Eles devem ser usados ao mesmo tempo.

Minificação no nível do pacote

A minificação no nível do pacote compacta todo o pacote após a compilação. Veja como funciona:

  1. Você escreve o código assim:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. O Webpack compila isso em aproximadamente o seguinte:

    // bundle.js (part of)
    "use strict";
    Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
    /* harmony export (immutable) */ __webpack_exports__["render"] = render;
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css__ = __webpack_require__(1);
    /* harmony import */ var __WEBPACK_IMPORTED_MODULE_0__comments_css_js___default =
    __webpack_require__.n(__WEBPACK_IMPORTED_MODULE_0__comments_css__);
    
    function render(data, target) {
    console.log('Rendered!');
    }
    
  3. Um minificador compacta o arquivo para algo como o seguinte:

    // minified bundle.js (part of)
    "use strict";function t(e,n){console.log("Rendered!")}
    Object.defineProperty(n,"__esModule",{value:!0}),n.render=t;var o=r(1);r.n(o)
    

No webpack 4, a minificação no nível do pacote é ativada automaticamente, tanto no modo de produção como sem um. Ele usa o minificador UglifyJS por trás. Se você precisar desativar a minificação, use o modo de desenvolvimento ou transmita false para a opção optimization.minimize.

No webpack 3, você precisa usar o plug-in UglifyJS diretamente. O plug-in é fornecido com o webpack. Para ativá-lo, adicione-o à seção plugins da configuração:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
  ],
};

Opções específicas do carregador

A segunda maneira de minimizar o código é com opções específicas do carregador (o que é um carregador). Com as opções do loader, é possível compactar coisas que o minificador não consegue. Por exemplo, quando você importa um arquivo CSS com css-loader, o arquivo é compilado em uma string:

/* comments.css */
.comment {
  color: black;
}
// minified bundle.js (part of)
exports=module.exports=__webpack_require__(1)(),
exports.push([module.i,".comment {\r\n  color: black;\r\n}",""]);

O minificador não pode compactar esse código porque ele é uma string. Para minimizar o conteúdo do arquivo, precisamos configurar o carregador para fazer isso:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { minimize: true } },
        ],
      },
    ],
  },
};

Leitura adicional

Especificar NODE_ENV=production

Outra maneira de diminuir o tamanho do front-end é definir a variável de ambiente NODE_ENV no código como o valor production.

As bibliotecas leem a variável NODE_ENV para detectar em qual modo elas precisam funcionar: em desenvolvimento ou produção. Algumas bibliotecas se comportam de maneira diferente com base nessa variável. Por exemplo, quando NODE_ENV não está definido como production, o Vue.js faz verificações adicionais e mostra avisos:

// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
  warn('props must be strings when using array syntax.');
}
// …

O React funciona de maneira semelhante, carregando um build de desenvolvimento que inclui os avisos:

// react/index.js
if (process.env.NODE_ENV === 'production') {
  module.exports = require('./cjs/react.production.min.js');
} else {
  module.exports = require('./cjs/react.development.js');
}

// react/cjs/react.development.js
// …
warning$3(
    componentClass.getDefaultProps.isReactClassApproved,
    'getDefaultProps is only used on classic React.createClass ' +
    'definitions. Use a static property named `defaultProps` instead.'
);
// …

Essas verificações e avisos geralmente não são necessários na produção, mas permanecem no código e aumentam o tamanho da biblioteca. No webpack 4,remova-as adicionando a opção optimization.nodeEnv: 'production':

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
};

No webpack 3,use DefinePlugin:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.DefinePlugin({
      'process.env.NODE_ENV': '"production"'
    }),
    new webpack.optimize.UglifyJsPlugin()
  ]
};

A opção optimization.nodeEnv e o DefinePlugin funcionam da mesma maneira: elas substituem todas as ocorrências de process.env.NODE_ENV pelo valor especificado. Com a configuração acima:

  1. O Webpack vai substituir todas as ocorrências de process.env.NODE_ENV por "production":

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if (process.env.NODE_ENV !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    
  2. Em seguida, o minificador remove todas as ramas if, porque "production" !== 'production' é sempre falso e o plug-in entende que o código dentro dessas ramificações nunca será executado:

    // vue/dist/vue.runtime.esm.js
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    } else if ("production" !== 'production') {
      warn('props must be strings when using array syntax.');
    }
    

    // vue/dist/vue.runtime.esm.js (without minification)
    if (typeof val === 'string') {
      name = camelize(val);
      res[name] = { type: null };
    }
    

Leitura adicional

Usar módulos ES

A próxima maneira de diminuir o tamanho do front-end é usar módulos ES.

Quando você usa módulos ES, o webpack passa a fazer o tree-shaking. O tree-shaking ocorre quando um bundler percorre toda a árvore de dependências, verifica quais dependências são usadas e remove as não utilizadas. Portanto, se você usar a sintaxe do módulo ES, o webpack poderá eliminar o código não usado:

  1. Você escreve um arquivo com várias exportações, mas o app usa apenas uma delas:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. O Webpack entende que commentRestEndpoint não é usado e não gera um ponto de exportação separado no pacote:

    // bundle.js (part that corresponds to comments.js)
    (function(module, __webpack_exports__, __webpack_require__) {
    "use strict";
    const render = () => { return 'Rendered!'; };
    /* harmony export (immutable) */ __webpack_exports__["a"] = render;
    
    const commentRestEndpoint = '/rest/comments';
    /* unused harmony export commentRestEndpoint */
    })
    
  3. O minificador remove a variável não usada:

    // bundle.js (part that corresponds to comments.js)
    (function(n,e){"use strict";var r=function(){return"Rendered!"};e.b=r})
    

Isso funciona mesmo com bibliotecas se elas forem escritas com módulos ES.

No entanto, não é necessário usar o minificador integrado (UglifyJsPlugin) do webpack. Qualquer minificador compatível com a remoção de código morto (por exemplo, plug-in do Babel Minify ou plug-in do Google Closure Compiler) vai funcionar.

Leitura adicional

Otimizar imagens

As imagens representam mais da metade do tamanho da página. Embora eles não sejam tão críticos quanto o JavaScript (por exemplo, eles não bloqueiam a renderização), eles ainda consomem uma grande parte da largura de banda. Use url-loader, svg-url-loader e image-webpack-loader para otimizar no webpack.

url-loader inlines pequenos arquivos estáticos no app. Sem configuração, ele pega um arquivo transmitido, coloca-o ao lado do pacote compilado e retorna um URL desse arquivo. No entanto, se especificarmos a opção limit, ela vai codificar arquivos menores que esse limite como um URL de dados Base64 e retornar esse URL. Isso insere a imagem no código JavaScript e salva uma solicitação HTTP:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif)$/,
        loader: 'url-loader',
        options: {
          // Inline files smaller than 10 kB (10240 bytes)
          limit: 10 * 1024,
        },
      },
    ],
  }
};
// index.js
import imageUrl from './image.png';
// → If image.png is smaller than 10 kB, `imageUrl` will include
// the encoded image: 'data:image/png;base64,iVBORw0KGg…'
// → If image.png is larger than 10 kB, the loader will create a new file,
// and `imageUrl` will include its url: `/2fcd56a1920be.png`

svg-url-loader funciona da mesma forma que url-loader, exceto que codifica arquivos com a codificação de URL em vez da Base64. Isso é útil para imagens SVG. Como os arquivos SVG são apenas um texto simples, essa codificação é mais eficiente em termos de tamanho.

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: "svg-url-loader",
        options: {
          limit: 10 * 1024,
          noquotes: true
        }
      }
    ]
  }
};

O image-webpack-loader compacta as imagens que passam por ele. Ele oferece suporte a imagens JPG, PNG, GIF e SVG, então vamos usá-lo para todos esses tipos.

Esse carregador não incorpora imagens ao app. Por isso, ele precisa funcionar em conjunto com url-loader e svg-url-loader. Para evitar copiar e colar nas duas regras (uma para imagens JPG/PNG/GIF e outra para SVG), vamos incluir esse loader como uma regra separada com enforce: 'pre':

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.(jpe?g|png|gif|svg)$/,
        loader: 'image-webpack-loader',
        // This will apply the loader before the other ones
        enforce: 'pre'
      }
    ]
  }
};

As configurações padrão do carregador já estão prontas para uso. No entanto, se você quiser fazer mais configurações, consulte as opções do plug-in. Para escolher as opções a serem especificadas, consulte o excelente guia de Addy Osmani sobre otimização de imagens.

Leitura adicional

Otimizar dependências

Mais da metade do tamanho médio do JavaScript vem de dependências, e parte desse tamanho pode ser desnecessária.

Por exemplo, o Lodash (a partir da v4.17.4) adiciona 72 KB de código minificado ao pacote. No entanto, se você usar apenas 20 dos métodos, aproximadamente 65 KB de código minimizado não farão nada.

Outro exemplo é o Moment.js. A versão 2.19.1 usa 223 KB de código reduzido, o que é enorme. O tamanho médio do JavaScript em uma página era de 452 KB em outubro de 2017. No entanto, 170 KB desse tamanho são arquivos de localização. Se você não usar o Moment.js com vários idiomas, esses arquivos vão aumentar o tamanho do pacote sem um propósito.

Todas essas dependências podem ser facilmente otimizadas. Reunimos abordagens de otimização em um repositório do GitHub. Confira.

Ativação da concatenação de módulos para módulos ES (também conhecidos como elevação de escopo)

Ao criar um pacote, o Webpack envolve cada módulo em uma função:

// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
  console.log('Rendered!');
}

// bundle.js (part  of)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });
  var __WEBPACK_IMPORTED_MODULE_0__comments_js__ = __webpack_require__(1);
  Object(__WEBPACK_IMPORTED_MODULE_0__comments_js__["a" /* render */])();
}),
/* 1 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  __webpack_exports__["a"] = render;
  function render(data, target) {
    console.log('Rendered!');
  }
})

No passado, isso era necessário para isolar os módulos CommonJS/AMD uns dos outros. No entanto, isso adicionou uma sobrecarga de tamanho e desempenho para cada módulo.

O Webpack 2 introduziu suporte a módulos ES que, ao contrário dos módulos CommonJS e AMD, podem ser agrupados sem envolver cada um com uma função. E o webpack 3 tornou esse agrupamento possível, com concatenação de módulos. Confira o que a concatenação de módulos faz:

// index.js
import {render} from './comments.js';
render();

// comments.js
export function render(data, target) {
  console.log('Rendered!');
}

// Unlike the previous snippet, this bundle has only one module
// which includes the code from both files

// bundle.js (part of; compiled with ModuleConcatenationPlugin)
/* 0 */
(function(module, __webpack_exports__, __webpack_require__) {
  "use strict";
  Object.defineProperty(__webpack_exports__, "__esModule", { value: true });

  // CONCATENATED MODULE: ./comments.js
    function render(data, target) {
    console.log('Rendered!');
  }

  // CONCATENATED MODULE: ./index.js
  render();
})

Percebeu a diferença? No pacote simples, o módulo 0 exigia render do módulo 1. Com a concatenação de módulos, require é simplesmente substituído pela função necessária, e o módulo 1 é removido. O pacote tem menos módulos e menos sobrecarga de módulo.

Para ativar esse comportamento no webpack 4, ative a opção optimization.concatenateModules:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    concatenateModules: true
  }
};

No webpack 3, use o ModuleConcatenationPlugin:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

Leitura adicional

Use externals se você tiver códigos do webpack e de outro tipo

Você pode ter um projeto grande em que alguns códigos são compilados com o webpack e outros não. Como um site de hospedagem de vídeos, em que o widget do player pode ser criado com o webpack, e a página pode não ser:

Captura de tela de um site de hospedagem de vídeos
(Um site de hospedagem de vídeos completamente aleatório)

Se os dois trechos de código tiverem dependências comuns, você poderá compartilhá-los para evitar o download do código várias vezes. Isso é feito com a opção externals do webpack: ela substitui módulos por variáveis ou outras importações externas.

Se as dependências estiverem disponíveis em window

Se o código que não é do webpack depender de dependências disponíveis como variáveis em window, crie um alias para os nomes de dependências e variáveis:

// webpack.config.js
module.exports = {
  externals: {
    'react': 'React',
    'react-dom': 'ReactDOM'
  }
};

Com essa configuração, o webpack não vai agrupar os pacotes react e react-dom. Em vez disso, elas serão substituídas por algo como este:

// bundle.js (part of)
(function(module, exports) {
  // A module that exports `window.React`. Without `externals`,
  // this module would include the whole React bundle
  module.exports = React;
}),
(function(module, exports) {
  // A module that exports `window.ReactDOM`. Without `externals`,
  // this module would include the whole ReactDOM bundle
  module.exports = ReactDOM;
})

Se as dependências forem carregadas como pacotes AMD

Se o código que não é do webpack não expor dependências para window, as coisas ficam mais complicadas. No entanto, ainda é possível evitar o carregamento do mesmo código duas vezes se o código que não é do webpack consumir essas dependências como pacotes AMD.

Para fazer isso, compile o código do webpack como um pacote AMD e crie um alias de módulos para os URLs da biblioteca:

// webpack.config.js
module.exports = {
  output: {
    libraryTarget: 'amd'
  },
  externals: {
    'react': {
      amd: '/libraries/react.min.js'
    },
    'react-dom': {
      amd: '/libraries/react-dom.min.js'
    }
  }
};

O Webpack vai agrupar o pacote em define() e fazer com que ele dependa destes URLs:

// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () { … });

Se o código que não é do webpack usar os mesmos URLs para carregar as dependências, esses arquivos serão carregados apenas uma vez. As solicitações adicionais vão usar o cache do carregador.

Leitura adicional

Resumo

  • Ative o modo de produção se você usa o webpack 4
  • Minimizar o código com as opções de minificador e carregador no nível do pacote
  • Remova o código exclusivo para desenvolvimento substituindo NODE_ENV por production
  • Usar módulos ES para ativar o shake de árvore
  • Compactar imagens
  • Aplicar otimizações específicas de dependência
  • Ativar a concatenação de módulos
  • Use externals se fizer sentido para você