Riduci dimensioni front-end

Come utilizzare webpack per ridurre il più possibile le dimensioni dell'app

Una delle prime cose da fare quando ottimizzi un'applicazione è ridurne il più possibile le dimensioni. Ecco come usare il webpack.

Utilizzare la modalità produzione (solo webpack 4)

Webpack 4 ha introdotto il nuovo flag mode. Potresti impostare questo flag su 'development' o 'production' per suggerire al 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 il 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 avviene quando comprimi il codice rimuovendo gli spazi aggiuntivi, accorciando i nomi delle variabili e così via. come illustrato di seguito:

// 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: la minimizzazione a livello di bundle e le opzioni specifiche del caricatore. e 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 come segue:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack lo compila approssimativamente nei 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 minificatore la comprime approssimativamente come segue:

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

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

Nel webpack 3, devi usare direttamente il plug-in UglifyJS. Il plug-in è incluso nel webpack; per attivarlo, aggiungilo alla sezione plugins del file di 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 sono le opzioni specifiche per il caricatore (che 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}",""]);

Il minifier non può comprimere questo codice perché è una stringa. Per minimizzare il contenuto del file, dobbiamo configurare il caricatore in questo modo:

// 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 la modalità di funzionamento, in fase di sviluppo o di produzione. Alcune librerie si comportano in modo diverso in base a questa variabile. Ad esempio, quando NODE_ENV non è impostato su production, Vue.js esegue controlli e avvisi aggiuntivi:

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

React 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 di solito non sono necessari in produzione, ma rimangono nel codice e aumentano le dimensioni della libreria. Nel webpack 4, rimuovile aggiungendo l'opzione optimization.nodeEnv: 'production':

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

Nel webpack 3, usa 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()
  ]
};

Sia l'opzione optimization.nodeEnv che 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 di questo tipo, poiché "production" !== 'production' è sempre false 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

Il prossimo modo per ridurre le dimensioni del front-end è utilizzare i moduli ES.

Quando utilizzi i moduli ES, il webpack è in grado di eseguire il tremolio degli alberi. Si verifica quando un bundler attraversa l'intero albero delle dipendenze, verifica quali dipendenze vengono utilizzate e rimuove quelle inutilizzate. Quindi, se usi la sintassi del modulo ES, il webpack può eliminare il codice inutilizzato:

  1. Anche se scrivi un file con più esportazioni, 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 approccio funziona anche con le librerie scritte con moduli ES.

Tuttavia, non è necessario utilizzare proprio il minificatore integrato del webpack (UglifyJsPlugin). Qualsiasi minificatore che supporti la rimozione di codici non validi (ad es. il plug-in Basbel Minify o il plug-in Google Closure Compiler) se ne occuperà.

Per approfondire

Ottimizza immagini

Le immagini rappresentano più della metà delle dimensioni della pagina. Sebbene non siano così importanti come JavaScript (ad es. non bloccano il rendering), utilizzano comunque una gran parte della larghezza di banda. Usa url-loader, svg-url-loader e image-webpack-loader per ottimizzarli nel webpack.

url-loader incorpora nell'app piccoli file statici. Senza configurazione, prende un file passato, lo inserisce accanto al bundle compilato e restituisce un URL di quel file. Tuttavia, se specifichiamo l'opzione limit, codificheremo i file più piccoli di questo limite 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, tranne per il fatto che codifica i file con la codifica URL anziché con quella Base64. Questo è utile per le immagini SVG. Poiché i file SVG sono solo testo normale, questa codifica riduce la dimensione dell'immagine.

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. Poiché supporta immagini JPG, PNG, GIF e SVG, la utilizzeremo per tutti questi tipi.

Questo caricatore non incorpora immagini nell'app, quindi deve funzionare in abbinamento 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, ma se vuoi configurarle ulteriormente, consulta le opzioni dei plug-in. Per scegliere quali opzioni specificare, consulta l'eccellente guida all'ottimizzazione delle immagini di Addy Osmani.

Per approfondire

Ottimizza le dipendenze

Più della metà delle dimensioni medie di JavaScript proviene da dipendenze e una parte di queste dimensioni potrebbe essere semplicemente inutile.

Ad esempio, Lodash (a partire dalla versione 4.17.4) aggiunge 72 kB di codice minimizzato al bundle. Ma se usi solo 20 metodi, circa 65 kB di codice minimizzato non fanno nulla.

Un altro esempio è Moment.js. La 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 a ottobre 2017. Tuttavia, 170 kB di queste dimensioni sono file di localizzazione. Se non utilizzi Moment.js con più lingue, questi file gonfieranno il bundle senza uno scopo.

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

Abilita la concatenazione dei moduli per i moduli ES (anche nota come sollevamento dell'ambito)

Quando crei un bundle, il webpack unisce 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 comportato un overhead in termini 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 eseguire il wrapping di ciascuno con una funzione. Inoltre, Webpack 3 ha reso possibile questo raggruppamento, grazie alla concatenazione dei moduli. Ecco come funziona la concatenazione di 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();
})

Vedi la differenza? Nel bundle semplice, il modulo 0 richiedeva render dal modulo 1. Con la concatenazione dei moduli, require viene semplicemente sostituito con la funzione richiesta e il modulo 1 viene rimosso. Il bundle ha meno moduli e meno overhead per i moduli.

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

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

Nel webpack 3,utilizza l'ModuleConcatenationPlugin:

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

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

Per approfondire

Utilizza externals se hai sia un codice webpack sia uno non webpack

Potresti avere un progetto di grandi dimensioni in cui alcune parti del codice vengono compilate con il webpack e altre no. Come un sito di hosting video, in cui il widget del player potrebbe essere realizzato con webpack e la pagina circostante potrebbe non essere:

Uno screenshot di un sito di hosting di video.
(Un sito di hosting video del tutto casuale)

Se entrambe le parti di codice hanno dipendenze comuni, puoi condividerle per evitare di scaricare il codice più volte. A questo scopo, utilizza 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, i nomi delle dipendenze degli alias rispetto ai nomi delle variabili:

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

Con questa configurazione, il webpack non raggruppa i pacchetti react e react-dom. Verranno invece sostituiti con qualcosa del genere:

// 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 sono più complicate. Tuttavia, puoi comunque evitare di caricare lo stesso codice due volte se il codice non webpack utilizza queste dipendenze come pacchetti AMD.

A questo scopo, compila il codice del webpack come bundle AMD e i moduli alias negli 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 fa 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 dipendenze, questi file verranno caricati una sola volta e le richieste aggiuntive utilizzeranno la cache del caricatore.

Per approfondire

Riepilogo

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