Diminuir o tamanho do front-end

Como usar o webpack para deixar seu app o menor possível.

Uma das primeiras ações ao otimizar um aplicativo é reduzir ao máximo o tamanho dele. 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 o 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 exclusivo para desenvolvimento em bibliotecas e muito mais.

Leia mais

Ativar minificação

A minificação ocorre 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 é compatível com duas maneiras de reduzir o código: a minificação no nível do pacote e opções específicas do carregador. Eles devem ser usados simultaneamente.

Minificação em nível de pacote

A minificação no nível do pacote compacta todo o pacote após a compilação. Confira 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 o compila aproximadamente da seguinte maneira:

    // 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 a compacta da seguinte forma:

    // 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 quanto sem esse recurso. Ele usa o minificador do UglifyJS em segundo plano. 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 vem 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 as opções específicas do carregador (o que é um carregador). Com as opções do carregador, é possível compactar itens que o minificador não consegue reduzir. 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 } },
        ],
      },
    ],
  },
};

Leia mais

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 como o valor production.

As bibliotecas leem a variável NODE_ENV para detectar em qual modo elas precisam funcionar: no desenvolvimento ou na produção. Algumas bibliotecas se comportam de maneira diferente com base nessa variável. Por exemplo, quando NODE_ENV não estiver definido como production, a Vue.js vai fazer outras verificações e mostrar 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. Ele carrega 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.'
);
// …

Esses avisos e verificações geralmente são desnecessários na produção, mas permanecem no código e aumentam o tamanho da biblioteca. No webpack 4, remova-os 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()
  ]
};

Tanto a opção optimization.nodeEnv quanto o DefinePlugin funcionam da mesma forma: 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. Depois, o minificador vai remover todas essas ramificações 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 };
    }
    

Leia mais

Usar módulos ES

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

Ao usar módulos ES, o webpack é capaz de 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 usadas. Portanto, se você usar a sintaxe do módulo ES, o webpack poderá eliminar o código não utilizado:

  1. Você grava 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 utilizada:

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

Isso funciona até mesmo com bibliotecas criadas com módulos ES.

No entanto, não é necessário usar precisamente o minificador integrado do webpack (UglifyJsPlugin). Qualquer minificador que ofereça suporte à remoção de código morto (por exemplo, o plug-in Babel Minify ou Google closure Compiler) funcionará.

Leia mais

Otimizar imagens

As imagens representam mais da metade do tamanho da página. Embora não sejam tão essenciais quanto o JavaScript (por exemplo, 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 otimizá-los no webpack.

O url-loader in-line pequenos arquivos estáticos no app. Sem a configuração, ele recebe 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 codificará arquivos menores que esse limite como um URL de dados Base64 e retornará esse URL. Isso incorpora a imagem ao 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`

O svg-url-loader funciona como url-loader, mas codifica arquivos com a codificação de URL em vez da codificação Base64. Isso é útil para imagens SVG, porque os arquivos SVG são apenas 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
        }
      }
    ]
  }
};

image-webpack-loader compacta as imagens que passam por ele. Ele é compatível com imagens JPG, PNG, GIF e SVG, então vamos usá-lo para todos esses tipos.

Esse carregador não incorpora imagens no app, então ele precisa funcionar com url-loader e svg-url-loader. Para evitar copiar e colar nas duas regras (uma para imagens JPG/PNG/GIF e outra para imagens SVG), incluiremos esse carregador 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, mas se você quiser configurá-lo em mais detalhes, consulte as opções de plug-in. Para escolher as opções a serem especificadas, consulte o excelente guia sobre otimização de imagens (em inglês) de Addy Osmani.

Leia mais

Otimizar dependências

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

Por exemplo, o Lodash (a partir da versão 4.17.4) adiciona 72 KB de código minificado ao pacote. No entanto, se você usar apenas 20 métodos, então aproximadamente 65 KB de código minificado não terá efeito.

Outro exemplo é o Moment.js. A versão 2.19.1 usa 223 KB de código minificado, o que é um tamanho enorme. O tamanho médio do JavaScript em uma página era 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 sobrecarregarão o pacote sem uma finalidade.

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

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

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

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

// 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()
  ]
};

Leia mais

Use externals se você tiver um código webpack e não 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:

Uma captura de tela de um site de hospedagem de vídeo
(Um site de hospedagem de vídeo completamente aleatório)

Se as duas partes do código tiverem dependências comuns, você pode compartilhá-las para evitar o download do código várias vezes. Isso é feito com a opção externals do webpack, que 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 é o webpack depender de dependências disponíveis como variáveis em window, atribua alias os nomes de dependência aos nomes de 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 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 são carregadas como pacotes AMD

Se o código que não é do webpack não expõe dependências no window, o processo é mais complicado. No entanto, ainda é possível evitar o carregamento do mesmo código duas vezes se o código que não está no webpack consumir essas dependências como pacotes AMD.

Para fazer isso, compile o código do webpack como um pacote AMD e atribua módulos de alias aos 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 unir 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 é o webpack usar os mesmos URLs para carregar as dependências, esses arquivos serão carregados apenas uma vez. Solicitações adicionais usarão o cache do carregador.

Leia mais

Resumo

  • Ativar o modo de produção se você usar o webpack 4
  • Minimize seu código com as opções de minificador e carregador 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
  • Compacte as imagens
  • Aplicar otimizações específicas à dependência
  • Ativar a concatenação do módulo
  • Use externals se isso fizer sentido para você