Riduci dimensioni front-end

Come utilizzare webpack per ridurre al minimo le dimensioni dell'app

Una delle prime cose da fare quando si ottimizza un'applicazione è ridurla il più possibile. Ecco come fare con webpack.

Webpack 4 ha introdotto il nuovo flag mode. Puoi impostare il flag su 'development' o 'production' per suggerire a webpack che stai creando l'applicazione per un ambiente specifico:

// webpack.config.js
module.exports = {
  mode: 'production',
};

Assicurati di attivare la modalità production quando crei la tua app per la produzione. In questo modo, webpack applicherà ottimizzazioni come la minimizzazione, la rimozione del codice solo per lo sviluppo nelle librerie e altro ancora.

Per approfondire

Abilita minimizzazione

La minimizzazione consiste nel comprimere il codice rimuovendo gli spazi extra, accorciando i nomi delle variabili e così via. Esempio:

// 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}

Webpack supporta due modi per minimizzare il codice: minificazione a livello di bundle e opzioni specifiche del caricatore. Devono essere utilizzati contemporaneamente.

Minimizzazione a livello di bundle

La minimizzazione a livello di bundle comprime l'intero bundle dopo la compilazione. Ecco come funziona:

  1. Scrivi il codice in questo modo:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack lo compila approssimativamente in base ai seguenti elementi:

    // 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. Un compressore lo comprime in modo approssimativo nel seguente modo:

    // 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)
    

In webpack 4, la minimizzazione a livello di bundle viene attivata automaticamente sia in modalità di produzione sia senza. Utilizza in background il minificatore UglifyJS. Se devi disattivare la minimizzazione, utilizza la modalità di sviluppo o passa false all'opzione optimization.minimize.

In webpack 3, devi utilizzare direttamente il plug-in UglifyJS. Il plug-in è incluso in webpack; per attivarlo, aggiungilo alla plugins sezione della configurazione:

// webpack.config.js
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.UglifyJsPlugin(),
  ],
};

Opzioni specifiche del caricatore

Il secondo modo per minimizzare il codice è rappresentato da opzioni specifiche del caricatore (cos'è un caricatore). Con le opzioni del caricatore puoi comprimere gli elementi che il minificatore non è in grado di minimizzare. Ad esempio, quando importi un file CSS con css-loader, il file viene compilato in una stringa:

/* 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}",""]);

Lo strumento di minificazione non può comprimere questo codice perché è una stringa. Per ridurre al minimo i contenuti del file, dobbiamo configurare il caricatore in modo da:

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /\.css$/,
        use: [
          'style-loader',
          { loader: 'css-loader', options: { minimize: true } },
        ],
      },
    ],
  },
};

Per approfondire

Specifica NODE_ENV=production

Un altro modo per ridurre le dimensioni del front-end è impostare la variabile di ambiente NODE_ENV nel codice sul valore production.

Le librerie leggono la variabile NODE_ENV per rilevare in quale modalità devono funzionare: in fase di sviluppo o di produzione. Alcune librerie si comportano in modo diverso in base a questa variabile. Ad esempio, se NODE_ENV non è impostato su production, Vue.js esegue controlli aggiuntivi e visualizza gli avvisi:

// vue/dist/vue.runtime.esm.js
// …
if (process.env.NODE_ENV !== 'production') {
  warn('props must be strings when using array syntax.');
}
// …

La reazione funziona in modo simile: carica una build di sviluppo che include gli avvisi:

// 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.'
);
// …

Questi controlli e avvisi in genere non sono necessari in produzione, ma rimangono nel codice e aumentano le dimensioni della libreria. In webpack 4, rimuovili aggiungendo l'opzione optimization.nodeEnv: 'production':

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    nodeEnv: 'production',
    minimize: true,
  },
};

In webpack 3, utilizza invece 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()
  ]
};

Entrambe l'opzione optimization.nodeEnv e DefinePlugin funzionano allo stesso modo: sostituiscono tutte le occorrenze di process.env.NODE_ENV con il valore specificato. Con la configurazione riportata sopra:

  1. Webpack sostituirà tutte le occorrenze di process.env.NODE_ENV con "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. Poi il minificatore rimuoverà tutti i rami if, perché "production" !== 'production' è sempre falso e il plug-in comprende che il codice all'interno di questi rami non verrà mai eseguito:

    // 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 };
    }
    

Per approfondire

Utilizzare i moduli ES

Un altro modo per ridurre le dimensioni del front-end è utilizzare i moduli ES.

Quando utilizzi i moduli ES, webpack diventa in grado di fare "albero di albero". Si verifica quando un bundler attraversa l'intero albero delle dipendenze, controlla quali dipendenze vengono utilizzate e rimuove quelle inutilizzate. Pertanto, se utilizzi la sintassi del modulo ES, webpack può eliminare il codice inutilizzato:

  1. Scrivi un file con più esportazioni, ma l'app ne utilizza solo una:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack riconosce che commentRestEndpoint non viene utilizzato e non genera un punto di esportazione separato nel bundle:

    // 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. Il minificatore rimuove la variabile inutilizzata:

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

Questo funziona anche con le librerie se sono scritte con moduli ES.

Tuttavia, non è necessario utilizzare esattamente il compressore integrato di webpack (UglifyJsPlugin). È sufficiente qualsiasi minificatore che supporti la rimozione di codice obsoleto (ad es. il plug-in Babel Minify o il plug-in Google Closure Compiler).

Per approfondire

Ottimizza immagini

Le immagini rappresentano più della metà delle dimensioni della pagina. Anche se non sono critici come JavaScript (ad es. non bloccano il rendering), occupano comunque gran parte della larghezza di banda. Utilizza url-loader, svg-url-loader e image-webpack-loader per ottimizzarle in webpack.

url-loader inserisce nell'app piccoli file statici. Senza configurazione, prende un file superato, lo inserisce accanto al bundle compilato e restituisce l'URL del file. Tuttavia, se specifichiamo l'opzione limit, i file di dimensioni inferiori a questo limite verranno codificati come URL di dati Base64 e restituirà questo URL. In questo modo l'immagine viene incorporata nel codice JavaScript e viene salvata una richiesta 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 funziona come url-loader, ma solo per il fatto che codifica i file con la codifica degli URL anziché con Base64. Questo è utile per le immagini SVG, poiché i file SVG sono semplicemente testo normale, questa codifica è più efficace per le dimensioni.

module.exports = {
  module: {
    rules: [
      {
        test: /\.svg$/,
        loader: "svg-url-loader",
        options: {
          limit: 10 * 1024,
          noquotes: true
        }
      }
    ]
  }
};

image-webpack-loader comprime le immagini che lo attraversano. Supporta le immagini JPG, PNG, GIF e SVG, quindi la utilizzeremo per tutti questi tipi.

Questo caricatore non incorpora immagini nell'app, quindi deve funzionare in combinazione con url-loader e svg-url-loader. Per evitare di copiarlo e incollarlo in entrambe le regole (una per le immagini JPG/PNG/GIF e un'altra per quelle SVG), includeremo questo caricatore come regola separata con 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'
      }
    ]
  }
};

Le impostazioni predefinite del caricatore sono già pronte per l'uso, ma se vuoi configurarlo ulteriormente, consulta le opzioni del plug-in. Per scegliere le opzioni da specificare, consulta l'eccellente guida all'ottimizzazione delle immagini di Addy Osmani.

Per approfondire

Ottimizza le dipendenze

Più della metà della dimensione media di JavaScript deriva dalle dipendenze e una parte di questa dimensione potrebbe essere semplicemente inutile.

Ad esempio, Lodash (a partire dalla versione 4.17.4) aggiunge 72 KB di codice compresso al bundle. Tuttavia, se ne utilizzi solo 20, circa 65 KB di codice compresso non fanno nulla.

Un altro esempio è Moment.js. La sua versione 2.19.1 richiede 223 kB di codice minimizzato, il che è enorme: la dimensione media di JavaScript in una pagina era di 452 kB nell'ottobre 2017. Tuttavia, 170 kB di quella dimensione sono file di localizzazione. Se non utilizzi Moment.js con più lingue, questi file gonfieranno il bundle senza scopo.

Tutte queste dipendenze possono essere facilmente ottimizzate. Abbiamo raccolto gli approcci all'ottimizzazione in un repository GitHub. Dai un'occhiata!

Attiva la concatenazione dei moduli per i moduli ES (noto anche come sollevamento degli ambiti)

Quando crei un bundle, webpack inserisce ogni modulo in una funzione:

// 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!');
  }
})

In passato, questa operazione era necessaria per isolare i moduli CommonJS/AMD l'uno dall'altro. Tuttavia, questo ha aggiunto un overhead di dimensioni e prestazioni per ogni modulo.

Webpack 2 ha introdotto il supporto per i moduli ES che, a differenza dei moduli CommonJS e AMD, possono essere raggruppati senza wrapping con una funzione. Inoltre, webpack 3 ha reso possibile questo raggruppamento, grazie alla concatenamento dei moduli. Ecco cosa fa la concatenazione dei moduli:

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

Capisci la differenza? Nel bundle semplice, il modulo 0 richiedeva render del modulo 1. Con la concatenazione del modulo, require viene semplicemente sostituito con la funzione richiesta e il modulo 1 viene rimosso. Il bundle ha meno moduli e un minore overhead dei moduli.

Per attivare questo comportamento, in webpack 4, abilita l'opzione optimization.concatenateModules:

// webpack.config.js (for webpack 4)
module.exports = {
  optimization: {
    concatenateModules: true
  }
};

In webpack 3, utilizza ModuleConcatenationPlugin:

// webpack.config.js (for webpack 3)
const webpack = require('webpack');

module.exports = {
  plugins: [
    new webpack.optimize.ModuleConcatenationPlugin()
  ]
};

Per approfondire

Utilizza externals se hai codice sia webpack che non webpack

Potresti avere un progetto di grandi dimensioni in cui parte del codice viene compilata con webpack e parte no. Ad esempio, un sito di hosting video, in cui il widget del player potrebbe essere creato con webpack e la pagina circostante potrebbe non esserlo:

Uno screenshot di un sito di hosting video
(Un sito di hosting video completamente casuale)

Se entrambe le parti del codice hanno dipendenze comuni, puoi condividerle per evitare di scaricare il codice più volte. Per farlo, usa l'opzione externals del webpack, che sostituisce i moduli con variabili o altre importazioni esterne.

Se le dipendenze sono disponibili in window

Se il codice non webpack si basa su dipendenze disponibili come variabili in window, utilizza i nomi delle dipendenze alias per i nomi delle variabili:

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

Con questa configurazione, webpack non raggruppa react e react-dom pacchetti. Verranno invece sostituiti con un testo simile al seguente:

// 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 le dipendenze vengono caricate come pacchetti AMD

Se il codice non webpack non espone dipendenze in window, le cose si complicano. Tuttavia, puoi comunque evitare di caricare lo stesso codice due volte se il codice non webpack consuma queste dipendenze come pacchetti AMD.

Per farlo, compila il codice webpack come bundle AMD e crea gli alias dei moduli per gli URL delle librerie:

// webpack.config.js
module.exports = {
  output: {
    libraryTarget: 'amd'
  },
  externals: {
    'react': {
      amd: '/libraries/react.min.js'
    },
    'react-dom': {
      amd: '/libraries/react-dom.min.js'
    }
  }
};

Webpack aggrega il bundle in define() e lo farà dipendere da questi URL:

// bundle.js (beginning)
define(["/libraries/react.min.js", "/libraries/react-dom.min.js"], function () {  });

Se il codice non webpack utilizza gli stessi URL per caricare le sue dipendenze, questi file verranno caricati solo una volta. Altre richieste utilizzeranno la cache del caricatore.

Per approfondire

Riepilogo

  • Attiva la modalità di produzione se utilizzi webpack 4
  • Minimizza il codice con le opzioni di minimizzazione e caricamento a livello di bundle
  • Rimuovi il codice solo per lo sviluppo sostituendo NODE_ENV con production
  • Utilizzare i moduli ES per consentire l'eliminazione degli alberi
  • Comprimi le immagini
  • Applica ottimizzazioni specifiche delle dipendenze
  • Abilita concatenazione dei moduli
  • Usa externals se questo è adatto alle tue esigenze