Diminuir o tamanho do front-end

Como usar o webpack para tornar 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 simultaneamente.

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 um código assim:

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

    // 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 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 vem empacotado 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 reduzir o código são 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 reduzir 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 seu código para o valor production.

As bibliotecas leem a variável NODE_ENV para detectar em qual modo precisam funcionar: no de desenvolvimento ou de 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, a Vue.js faz outras verificações 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 forma semelhante. Ele carrega uma versão 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 são desnecessá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 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 minifiador removerá todas essas ramificações if, já que "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 pode fazer o tree shaking. Tree-shaking ocorre quando um bundler passa por toda a árvore de dependências, verifica quais dependências estão sendo usadas e remove aquelas 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 minimizador remove a variável não utilizada:

    // 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 que foram criadas 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.

O url-loader coloca pequenos arquivos estáticos inline no app. Sem configuração, ele pega um arquivo transmitido, o coloca ao lado do pacote compilado e retorna um URL desse arquivo. No entanto, se especificarmos a opção limit, ela 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 como url-loader, mas codifica arquivos com a codificação de URL em vez da Base64. Isso é útil para imagens SVG, porque os arquivos SVG são apenas um texto simples, essa codificação é mais eficaz 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 no app, portanto, 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 quais opções especificar, confira o excelente guia sobre otimização de imagens (em inglês) da Addy Osmani.

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, Lodash (a partir da v4.17.4) adiciona 72 KB de código minificado ao pacote. No entanto, se você usar apenas 20 métodos, aproximadamente 65 KB de código minificado não terá efeito.

Outro exemplo é o Moment.js. A versão 2.19.1 ocupa 223 KB de código minificado, o que é enorme. O tamanho médio de JavaScript em uma página era de 452 KB em outubro de 2017 (link em inglês). 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.

Ativar a concatenação de módulos para módulos ES (também conhecido 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!');
  }
})

Antes isso era necessário para isolar os módulos CommonJS/AMD uns dos outros. No entanto, isso aumentou o tamanho e o desempenho de 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 possibilitou esse agrupamento, usando a 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 o 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 um código webpack e outro que não seja webpack

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ídeo, em que o widget do player pode ser criado com o webpack e a página ao redor pode não ser:

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

Se os dois trechos de código tiverem dependências em comum, você poderá compartilhá-los para evitar o download do código várias vezes. Isso é feito com a opção externals do webpack, que substitui os 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, nomes de dependência de alias para nomes de variáveis:

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

Com esta configuração, o webpack não agrupa os pacotes react e react-dom. Em vez disso, eles serão substituídos por algo assim:

// 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 carregar o 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 use módulos de alias para URLs de 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 um código que não é do webpack usar os mesmos URLs para carregar suas dependências, esses arquivos serão carregados somente uma vez. As solicitações adicionais usarão o cache do carregador.

Leitura adicional

Resumo

  • Ativar o modo de produção se você usar o webpack 4
  • Minimize seu código com as opções de carregador e minifiador no nível do pacote
  • Remova o código somente para desenvolvimento substituindo NODE_ENV por production.
  • Usar módulos ES para ativar o tree shaking
  • Compactar imagens
  • Aplicar otimizações específicas de dependência
  • Ativar a concatenação de módulos
  • Use externals se isso fizer sentido para você