Riduci dimensioni front-end

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

Una delle prime cose da fare quando ottimizzi un'applicazione è ridurla al minimo. Ecco come fare con webpack.

Utilizza la modalità di produzione (solo webpack 4)

Webpack 4 ha introdotto il nuovo flag mode. Puoi impostare questo 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 sviluppi l'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

Attiva la 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 ridurre il codice: la minimizzazione a livello di bundle e le 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 come segue:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack lo compila in modo approssimativo nel seguente modo:

    // 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 produzione sia in assenza di questa modalità. Utilizza il minificatore UglifyJS in background. 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 ridurre il codice in modo minimo è utilizzare le opzioni specifiche del caricatore (che cos'è un caricatore). Con le opzioni del caricatore, puoi comprimere elementi che il minimizzatore non può 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 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 NODE_ENV variabile di ambiente nel codice sul valore production.

Le librerie leggono la variabile NODE_ENV per rilevare in quale modalità devono funzionare, in quella 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 aggiuntivi e stampa avvisi:

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

Sia l'opzione optimization.nodeEnv sia 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. Il minimizzatore rimuoverà tutti questi 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 eseguire il tree-shaking. Il tree-shaking si verifica quando un bundler esamina l'intero albero delle dipendenze, controlla quali 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 comprende che commentRestEndpoint non viene utilizzato e non genera un punto di esportazione distinto 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 i moduli ES.

Tuttavia, non è necessario utilizzare esattamente il compressore integrato di webpack (UglifyJsPlugin). Qualsiasi compressore che supporta la rimozione del codice inutilizzato (ad es. il plugin Babel Minify o il plugin Google Closure Compiler) andrà bene.

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 ottimizzarli in webpack.

url-loader inserisce piccoli file statici in linea nell'app. Senza configurazione, prende un file passato, lo inserisce accanto al bundle compilato e restituisce un 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 verrà restituito questo URL. In questo modo, l'immagine viene inserita in linea 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 codifica i file con la codifica URL anziché con quella Base64. Questo è utile per le immagini SVG, perché i file SVG sono solo testo normale, quindi questa codifica è più efficace in termini di 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 lo utilizzeremo per tutti questi tipi.

Questo caricatore non incorpora le immagini nell'app, quindi deve funzionare in coppia 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 sull'ottimizzazione delle immagini di Addy Osmani.

Per approfondire

Ottimizza le dipendenze

Più della metà delle dimensioni medie di JavaScript è costituita da dipendenze e parte di queste dimensioni potrebbe essere semplicemente non necessaria.

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 versione 2.19.1 occupa 223 KB di codice compresso, un valore enorme: le dimensioni medie di JavaScript su una pagina erano 452 KB a ottobre 2017. Tuttavia, 170 KB di queste dimensioni sono costituiti da 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 di ottimizzazione in un repository GitHub. Dai un'occhiata.

Attivare la concatenazione dei moduli per i moduli ES (ovvero l'elevazione dell'ambito)

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, questo era necessario per isolare i moduli CommonJS/AMD l'uno dall'altro. Tuttavia, questo ha comportato un aumento delle dimensioni e delle 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 avvolgere ciascuno con una funzione. Webpack 3 ha reso possibile questo tipo di aggregazione con la concatenazione 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 normale, 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 un minore overhead dei moduli.

Per attivare questo comportamento, in webpack 4, attiva 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 entrambi i codici hanno dipendenze comuni, puoi condividerli per evitare di scaricarli più volte. Questo viene eseguito con l'opzione externals di 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, crea un alias per i nomi delle dipendenze ai nomi delle variabili:

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

Con questa configurazione, webpack non raggruppa i pacchetti react e react-dom. Verranno invece sostituiti con qualcosa di 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 utilizza 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 inserirà il bundle in define() e lo renderà dipendente 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 solo una volta; le richieste aggiuntive 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 abilitare lo shaking dell'albero
  • Comprimi le immagini
  • Applicare ottimizzazioni specifiche per le dipendenze
  • Abilita la concatenazione dei moduli
  • Utilizza externals se ha senso per te