Mengurangi ukuran front-end

Cara menggunakan webpack untuk membuat aplikasi sekecil mungkin

Salah satu hal pertama yang harus dilakukan saat Anda mengoptimalkan aplikasi adalah membuatnya sekecil mungkin. Berikut cara melakukannya dengan webpack.

Menggunakan mode produksi (khusus webpack 4)

Webpack 4 memperkenalkan flag mode baru. Anda dapat menetapkan flag ini ke 'development' atau 'production' guna memberikan petunjuk kepada webpack bahwa Anda membangun aplikasi untuk lingkungan tertentu:

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

Pastikan Anda mengaktifkan mode production saat membangun aplikasi untuk produksi. Hal ini akan membuat webpack menerapkan pengoptimalan seperti minifikasi, penghapusan kode khusus pengembangan di library, dan lainnya.

Bacaan lebih lanjut

Mengaktifkan minifikasi

Minifikasi adalah saat Anda mengompresi kode dengan menghapus spasi tambahan, mempersingkat nama variabel, dan sebagainya. Seperti ini:

// 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 mendukung dua cara untuk meminifikasi kode: minifikasi tingkat paket dan opsi khusus loader. Keduanya harus digunakan secara bersamaan.

Minifikasi tingkat paket

Minifikasi level paket mengompresi seluruh paket setelah kompilasi. Berikut cara kerjanya:

  1. Anda menulis kode seperti ini:

    // comments.js
    import './comments.css';
    export function render(data, target) {
      console.log('Rendered!');
    }
    
  2. Webpack mengompilasinya menjadi sekitar hal berikut:

    // 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. Minifier mengompresinya menjadi kira-kira berikut:

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

Di webpack 4, minifikasi tingkat paket diaktifkan secara otomatis – baik dalam mode produksi maupun tanpa mode. Ia menggunakan minifier UglifyJS di balik layar. (Jika perlu menonaktifkan minifikasi, cukup gunakan mode pengembangan atau teruskan false ke opsi optimization.minimize.)

Di webpack 3, Anda harus menggunakan plugin UglifyJS secara langsung. Plugin dilengkapi dengan webpack. Untuk mengaktifkannya, tambahkan ke bagian plugins pada konfigurasi:

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

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

Opsi khusus loader

Cara kedua untuk meminifikasi kode adalah opsi khusus loader (apa itu loader). Dengan opsi loader, Anda dapat mengompresi hal-hal yang tidak dapat diminifikasi oleh minifier. Misalnya, saat Anda mengimpor file CSS dengan css-loader, file akan dikompilasi menjadi 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}",""]);

Minifier tidak dapat mengompresi kode ini karena berupa string. Untuk meminifikasi konten file, kita perlu mengonfigurasi loader untuk melakukan hal ini:

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

Bacaan lebih lanjut

Tentukan NODE_ENV=production

Cara lain untuk mengurangi ukuran front-end adalah dengan menetapkan variabel lingkungan NODE_ENV dalam kode Anda ke nilai production.

Library membaca variabel NODE_ENV untuk mendeteksi mode mana yang harus dijalankan – dalam pengembangan atau produksi. Beberapa library berperilaku berbeda berdasarkan variabel ini. Misalnya, jika NODE_ENV tidak ditetapkan ke production, Vue.js akan melakukan pemeriksaan tambahan dan mencetak peringatan:

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

React bekerja dengan cara yang sama – ia memuat build pengembangan yang menyertakan peringatan:

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

Pemeriksaan dan peringatan tersebut biasanya tidak diperlukan dalam produksi, tetapi tetap ada dalam kode dan meningkatkan ukuran library. Di webpack 4, hapus konfigurasi tersebut dengan menambahkan opsi optimization.nodeEnv: 'production':

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

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

Opsi optimization.nodeEnv dan DefinePlugin berfungsi dengan cara yang sama, keduanya mengganti semua kemunculan process.env.NODE_ENV dengan nilai yang ditentukan. Dengan konfigurasi dari atas:

  1. Webpack akan mengganti semua kemunculan process.env.NODE_ENV dengan "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. Kemudian minifier akan menghapus semua cabang if tersebut – karena "production" !== 'production' selalu bernilai salah (false) dan plugin memahami bahwa kode di dalam cabang ini tidak akan pernah dijalankan:

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

Bacaan lebih lanjut

Menggunakan modul ES

Cara berikutnya untuk mengurangi ukuran front-end adalah dengan menggunakan modul ES.

Jika Anda menggunakan modul ES, webpack dapat melakukan tree-shaking. Tree-shaking adalah ketika pemaket melintasi seluruh hierarki dependensi, memeriksa dependensi mana yang digunakan, dan menghapus dependensi yang tidak digunakan. Jadi, jika Anda menggunakan sintaksis modul ES, webpack dapat menghapus kode yang tidak digunakan:

  1. Anda menulis file dengan beberapa ekspor, tetapi aplikasi hanya menggunakan salah satunya:

    // comments.js
    export const render = () => { return 'Rendered!'; };
    export const commentRestEndpoint = '/rest/comments';
    
    // index.js
    import { render } from './comments.js';
    render();
    
  2. Webpack memahami bahwa commentRestEndpoint tidak digunakan dan tidak membuat titik ekspor terpisah dalam paket:

    // 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. Minifier menghapus variabel yang tidak digunakan:

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

Ini berfungsi bahkan dengan library jika ditulis dengan modul ES.

Namun, Anda tidak diharuskan menggunakan minifier bawaan webpack (UglifyJsPlugin) secara persis. Minifier apa pun yang mendukung penghapusan kode yang mati (misalnya plugin Babel Minify atau plugin Google Closure Compiler) akan membantu.

Bacaan lebih lanjut

Mengoptimalkan gambar

Gambar menempati lebih dari setengah ukuran halaman. Meskipun tidak sepenting JavaScript (misalnya, JavaScript tidak memblokir rendering), JavaScript tetap memakan sebagian besar bandwidth. Gunakan url-loader, svg-url-loader, dan image-webpack-loader untuk mengoptimalkannya di webpack.

url-loader menyisipkan file statis kecil ke dalam aplikasi. Tanpa konfigurasi, file akan mengambil file yang diteruskan, lalu menempatkannya di samping paket yang dikompilasi, dan menampilkan URL file tersebut. Namun, jika kita menentukan opsi limit, opsi ini akan mengenkode file yang lebih kecil dari batas ini sebagai URL data Base64 dan menampilkan URL ini. Tindakan ini akan menyisipkan gambar ke dalam kode JavaScript dan menyimpan permintaan 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 berfungsi seperti url-loader – kecuali bahwa kode ini mengenkode file dengan encoding URL, bukan yang Base64. Hal ini berguna untuk gambar SVG karena file SVG hanya berupa teks biasa, encoding ini lebih efektif ukuran.

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

image-webpack-loader mengompresi gambar yang melewatinya. Ini mendukung gambar JPG, PNG, GIF dan SVG, jadi kita akan menggunakannya untuk semua jenis ini.

Loader ini tidak menyematkan gambar ke dalam aplikasi sehingga harus berfungsi bersama url-loader dan svg-url-loader. Agar tidak perlu menyalin dan menempelnya ke kedua aturan (satu untuk gambar JPG/PNG/GIF, dan satu lagi untuk gambar SVG), kami akan menyertakan loader ini sebagai aturan terpisah dengan 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'
      }
    ]
  }
};

Setelan default loader sudah siap digunakan – tetapi jika Anda ingin mengonfigurasinya lebih lanjut, lihat opsi plugin. Untuk memilih opsi yang akan ditentukan, lihat panduan pengoptimalan gambar Addy Osmani yang sangat bagus.

Bacaan lebih lanjut

Mengoptimalkan dependensi

Lebih dari setengah ukuran JavaScript rata-rata berasal dari dependensi, dan sebagian dari ukuran tersebut mungkin tidak diperlukan.

Misalnya, Lodash (per v4.17.4) menambahkan 72 KB kode yang diminifikasi ke paket tersebut. Namun, jika Anda hanya menggunakan, misalnya, 20 metodenya, maka sekitar 65 KB kode yang diminifikasi tidak akan berfungsi sama sekali.

Contoh lainnya adalah Moment.js. Versi 2.19.1-nya membutuhkan kode yang diminifikasi sebesar 223 KB, yang sangat besar – rata-rata ukuran JavaScript di halaman adalah 452 KB pada bulan Oktober 2017. Namun, 170 KB dari ukuran tersebut adalah file pelokalan. Jika Anda tidak menggunakan Moment.js dengan beberapa bahasa, file ini akan menggelembungkan paket tanpa tujuan.

Semua dependensi ini bisa dioptimalkan dengan mudah. Kami telah mengumpulkan pendekatan pengoptimalan di repo GitHub – lihat di sini.

Mengaktifkan penggabungan modul untuk modul ES (alias pengangkatan cakupan)

Saat Anda mem-build paket, webpack menggabungkan setiap modul ke dalam sebuah fungsi:

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

Sebelumnya, modul ini diperlukan untuk mengisolasi modul CommonJS/AMD dari satu sama lain. Namun, hal ini menambah overhead ukuran dan performa untuk setiap modul.

Tidak seperti modul CommonJS dan AMD, Webpack 2 memperkenalkan dukungan untuk modul ES yang dapat dipaketkan tanpa menggabungkan masing-masing dengan fungsi. Dan webpack 3 memungkinkan pemaketan tersebut – dengan penggabungan modul. Berikut adalah fungsi penggabungan modul:

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

Lihat perbedaannya? Dalam paket biasa, modul 0 memerlukan render dari modul 1. Dengan penggabungan modul, require hanya diganti dengan fungsi yang diperlukan, dan modul 1 dihapus. Paket ini memiliki lebih sedikit modul – dan lebih sedikit overhead modul.

Untuk mengaktifkan perilaku ini, di webpack 4, aktifkan opsi optimization.concatenateModules:

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

Di webpack 3, gunakan ModuleConcatenationPlugin:

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

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

Bacaan lebih lanjut

Gunakan externals jika Anda memiliki kode webpack dan non-webpack

Anda mungkin memiliki project besar dengan beberapa kode dikompilasi dengan webpack, tetapi beberapa kode lainnya tidak. Seperti situs hosting video, yang memungkinkan widget pemutar dibuat dengan webpack, dan halaman di sekitarnya mungkin tidak:

Screenshot situs hosting video
(Situs hosting video yang sepenuhnya acak)

Jika kedua bagian kode memiliki dependensi yang sama, Anda dapat membagikannya untuk menghindari download kode berkali-kali. Hal ini dilakukan dengan opsi externals webpack – yang menggantikan modul dengan variabel atau impor eksternal lainnya.

Jika dependensi tersedia di window

Jika kode non-webpack Anda bergantung pada dependensi yang tersedia sebagai variabel dalam window, nama dependensi alias ke nama variabel:

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

Dengan konfigurasi ini, webpack tidak akan memaketkan paket react dan react-dom. Sebagai gantinya, string tersebut akan diganti dengan kode seperti ini:

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

Jika dependensi dimuat sebagai paket AMD

Jika kode non-webpack Anda tidak mengekspos dependensi ke window, masalahnya akan lebih rumit. Namun, Anda tetap dapat menghindari pemuatan kode yang sama dua kali jika kode non-webpack menggunakan dependensi ini sebagai paket AMD.

Untuk melakukannya, kompilasi kode webpack sebagai modul alias dan paket AMD ke URL library:

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

Webpack akan menggabungkan paket ke define() dan membuatnya bergantung pada URL berikut:

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

Jika kode non-webpack menggunakan URL yang sama untuk memuat dependensinya, file ini hanya akan dimuat satu kali - permintaan tambahan akan menggunakan cache loader.

Bacaan lebih lanjut

Mengambil kesimpulan

  • Aktifkan mode produksi jika Anda menggunakan webpack 4
  • Minimalkan kode dengan opsi minifier dan loader tingkat paket
  • Hapus kode khusus pengembangan dengan mengganti NODE_ENV dengan production
  • Menggunakan modul ES untuk mengaktifkan tree shaking
  • Kompresi gambar
  • Menerapkan pengoptimalan khusus dependensi
  • Mengaktifkan penyambungan modul
  • Gunakan externals jika sesuai untuk Anda