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:
Você escreve um código assim:
// comments.js import './comments.css'; export function render(data, target) { console.log('Rendered!'); }
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!'); }
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
- 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 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:
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.'); }
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
- 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 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:
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 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
Documentos 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.
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
- Documentação do Webpack para o ModuleConcatenationPlugin.
- “Breve introdução à elevação de escopo”
- Descrição detalhada do que esse plug-in faz (link em inglês).
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:
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
- Documentos do Webpack em
externals
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
porproduction
. - 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ê