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 mais pequeno sempre que 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. Você pode definir essa flag para '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 e remoção de código somente para desenvolvimento. em bibliotecas e muito mais.

Leitura adicional

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 (link em inglês) e opções específicas do carregador. 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 a compacta aproximadamente 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 na produção e sem um. Ele usa o minifiador UglifyJS. nos bastidores. Se precisar desativar a minificação, basta usar 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 ao 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 é). Com opções de carregadores, é possível compactar que o redutor não possa 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 redutor não pode compactar este código porque ele é uma string. Para reduzir o conteúdo do arquivo, precisamos configure 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 o NODE_ENV variável de ambiente no código para o valor production.

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

// 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 aumentar 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 a DefinePlugin funcionam da mesma maneira, elas substituem todas as ocorrências de process.env.NODE_ENV pelo valor especificado. Com 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. E o redutor remove todos esses ramificações if: como "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 ES módulos.

Quando você usa módulos ES, o webpack pode fazer o tree shaking. Tree shaking é quando um bundler percorre toda a árvore de dependências, verifica quais dependências estão sendo usadas e remove as não utilizadas. Então, 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 o commentRestEndpoint não é usado e não gera uma 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 exatamente o minifiador integrado do webpack (UglifyJsPlugin). Qualquer minificador compatível com a remoção de códigos inativos Por exemplo, o plug-in Babel Minify (link em inglês). ou o plug-in Google Closure Compiler (em inglês) é a solução.

Leitura adicional

Otimizar imagens

As imagens representam mais de um metade do tamanho da página. Enquanto eles não são tão importantes quanto o JavaScript (por exemplo, não bloqueiam a renderização), mas ainda consomem uma grande parte a largura de banda. Use url-loader, svg-url-loader e image-webpack-loader para otimizá-las de webpack.

O url-loader alinha arquivos estáticos pequenos à app. Sem a 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 retorna esse URL. Isso alinha 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`

svg-url-loader funciona como url-loader: exceto por codificar os arquivos com o URL codificação em vez da Base64 um. Isso é útil para imagens SVG, pois 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 por ele. Ele oferece suporte a imagens JPG, PNG, GIF e SVG, por isso vamos usá-lo para todos esses tipos.

Esse carregador não incorpora imagens no app, por isso precisa funcionar em conjunto com url-loader e svg-url-loader. Para evitar a cópia e colagem em ambas as regras (uma para imagens JPG/PNG/GIF e outra para imagens JPG/PNG/GIF) uma para 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 mais, consulte as opções do plug-in. Para escolher as opções para especificar, confira o excelente guia sobre imagem de Addy Osmani de otimização.

Leitura adicional

Otimizar dependências

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

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

Outro exemplo é o Moment.js. Sua 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. No entanto, 170 KB desse tamanho é a localização .tf. Se se você não usar o Moment.js com vários idiomas, esses arquivos ocuparão o pacote sem um do propósito da tecnologia.

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 acrescentou uma sobrecarga de tamanho e desempenho para cada módulo.

O Webpack 2 introduziu suporte para 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 concatenção de módulos. Aqui está 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();
})

Vê a diferença? No pacote simples, o módulo 0 exigia o render do módulo 1. Com concatenação de módulos, require é simplesmente substituído pela função necessária, e o módulo 1 é removida. 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

É possível que você tenha um projeto grande em que parte do código é compilado com o webpack e outros não. Gostei 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 os dois códigos tiverem dependências em comum, você poderá compartilhá-los para evitar o download do código várias vezes. Isso é feito com o externals do webpack option: substitui módulos por variáveis ou e 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, o alias nomes de dependência a 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ído por algo como:

// 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 ao window, o processo será mais complicado. No entanto, ainda é possível evitar carregar o mesmo código duas vezes se o código que não é do pacote da Web consumir esses dependências como pacotes AMD.

Para fazer isso, compile o código webpack como um pacote AMD e 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 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 um código que não é do webpack usar os mesmos URLs para carregar as próprias 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 da dependência
  • Ativar a concatenação de módulos
  • Use externals se isso fizer sentido para você