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:
Você escreve o código assim:
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
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!'); }
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
- Documentação do UglifyJsPlugin
- Outros minificadores conhecidos: Babel Minify, Google Closure Compiler
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:
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.'); }
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
- O que são as "variáveis de ambiente"
- Documentos do Webpack sobre:
DefinePlugin
,EnvironmentPlugin
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:
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();
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 */ })
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
Documentação do Webpack sobre o tree shaking
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
- Documentos do Webpack para o ModuleConcatenationPlugin
- "Brief introduction to scope hoisting" (em inglês)
- Descrição detalhada de o que este plug-in faz
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:
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
- Documentação do Webpack no
externals
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
porproduction
- 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ê