Menayangkan kode modern ke browser modern untuk pemuatan halaman yang lebih cepat

Dalam codelab ini, tingkatkan performa aplikasi sederhana ini yang memungkinkan pengguna memberi rating kucing acak. Pelajari cara mengoptimalkan paket JavaScript dengan meminimalkan jumlah kode yang ditranspilasi.

Screenshot aplikasi

Dalam aplikasi contoh, Anda dapat memilih kata atau emoji untuk menunjukkan seberapa besar Anda menyukai setiap kucing. Saat Anda mengklik tombol, aplikasi akan menampilkan nilai tombol di bawah gambar kucing saat ini.

Ukur

Sebaiknya mulai dengan memeriksa situs sebelum menambahkan pengoptimalan apa pun:

  1. Untuk melihat pratinjau situs, tekan Lihat Aplikasi. Lalu tekan Layar Penuh layar penuh.
  2. Tekan `Control+Shift+J` (atau `Command+Option+J` di Mac) untuk membuka DevTools.
  3. Klik tab Network
  4. Centang kotak Disable cache.
  5. Muat ulang aplikasi.

Permintaan ukuran paket asli

Lebih dari 80 KB digunakan untuk aplikasi ini! Saatnya untuk mengetahui apakah bagian dari paket tidak digunakan:

  1. Tekan Control+Shift+P (atau Command+Shift+P di Mac) untuk membuka menu Command. Menu Perintah

  2. Masukkan Show Coverage dan tekan Enter untuk menampilkan tab Cakupan.

  3. Di tab Cakupan, klik Reload untuk memuat ulang aplikasi saat mengambil cakupan.

    Muat ulang aplikasi dengan cakupan kode

  4. Lihat jumlah kode yang digunakan dibandingkan jumlah kode yang dimuat untuk paket utama:

    Cakupan kode paket

Lebih dari separuh paket (44 KB) bahkan tidak digunakan. Ini dikarenakan banyak kode di dalamnya terdiri dari polyfill untuk memastikan aplikasi berfungsi di browser yang lebih lama.

Menggunakan @babel/preset-env

Sintaksis bahasa JavaScript sesuai dengan standar yang disebut ECMAScript, atau ECMA-262. Versi spesifikasi yang lebih baru dirilis setiap tahun dan menyertakan fitur baru yang telah lulus dalam proses proposal. Setiap browser utama selalu berada di tahap berbeda dalam mendukung fitur ini.

Fitur ES2015 berikut digunakan dalam aplikasi:

Fitur ES2017 berikut juga digunakan:

Jangan ragu untuk mempelajari kode sumber di src/index.js untuk melihat bagaimana semua ini digunakan.

Semua fitur ini didukung dalam versi terbaru Chrome, tetapi bagaimana dengan browser lain yang tidak mendukungnya? Babel, yang disertakan dalam aplikasi, adalah library paling populer yang digunakan untuk mengompilasi kode yang berisi sintaksis baru ke dalam kode yang dapat dipahami oleh browser dan lingkungan lama. Cara ini dilakukan dengan dua cara:

  • Polyfill disertakan untuk mengemulasi fungsi ES2015+ yang lebih baru sehingga API-nya dapat digunakan meskipun tidak didukung oleh browser. Berikut adalah contoh polyfill dari metode Array.includes.
  • Plugin digunakan untuk mengubah kode ES2015 (atau yang lebih baru) menjadi sintaksis ES5 yang lebih lama. Karena ini adalah perubahan terkait sintaksis (seperti fungsi panah), perubahan ini tidak dapat diemulasikan dengan polyfill.

Lihat package.json untuk melihat library Babel mana yang disertakan:

"dependencies": {
  "@babel/polyfill": "^7.0.0"
},
"devDependencies": {
  //...
  "babel-loader": "^8.0.2",
  "@babel/core": "^7.1.0",
  "@babel/preset-env": "^7.1.0",
  //...
}
  • @babel/core adalah compiler Babel inti. Dengan demikian, semua konfigurasi Babel akan ditentukan dalam .babelrc pada root project.
  • babel-loader menyertakan Babel dalam proses build webpack.

Sekarang lihat webpack.config.js untuk mengetahui bagaimana babel-loader disertakan sebagai aturan:

module: {
  rules: [
    //...
    {
      test: /\.js$/,
      exclude: /node_modules/,
      loader: "babel-loader"
    }
  ]
},
  • @babel/polyfill menyediakan semua polyfill yang diperlukan untuk fitur ECMAScript yang lebih baru sehingga dapat berfungsi di lingkungan yang tidak mendukungnya. File ini sudah diimpor di bagian paling atas src/index.js.
import "./style.css";
import "@babel/polyfill";
  • @babel/preset-env mengidentifikasi transformasi dan polyfill mana yang diperlukan untuk setiap browser atau lingkungan yang dipilih sebagai target.

Lihat file konfigurasi Babel, .babelrc, untuk melihat bagaimana file tersebut disertakan:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions"
      }
    ]
  ]
}

Ini adalah penyiapan Babel dan webpack. Pelajari cara menyertakan Babel dalam aplikasi jika Anda kebetulan menggunakan pemaket modul yang berbeda dari webpack.

Atribut targets di .babelrc mengidentifikasi browser yang ditarget. @babel/preset-env terintegrasi dengan daftar browser, yang berarti Anda dapat menemukan daftar lengkap kueri kompatibel yang dapat digunakan di kolom ini dalam dokumentasi daftar browser.

Nilai "last 2 versions" melakukan transpilasi kode dalam aplikasi untuk dua versi terakhir setiap browser.

Proses debug

Untuk melihat semua target Babel browser serta semua transformasi dan polyfill yang disertakan, tambahkan kolom debug ke .babelrc:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
      }
    ]
  ]
}
  • Klik Tools.
  • Klik Log.

Muat ulang aplikasi dan lihat log status Glitch di bagian bawah editor.

Browser yang ditargetkan

Babel mencatat sejumlah detail ke konsol tentang proses kompilasi, termasuk semua lingkungan target tempat kode dikompilasi.

Browser yang ditargetkan

Perhatikan bagaimana browser yang dihentikan, seperti Internet Explorer, disertakan dalam daftar ini. Ini menjadi masalah karena browser yang tidak didukung tidak akan memiliki fitur baru yang ditambahkan, dan Babel terus mentranspilasi sintaksis khusus untuk browser tersebut. Hal ini akan meningkatkan ukuran paket Anda secara tidak perlu jika pengguna tidak menggunakan browser ini untuk mengakses situs Anda.

Babel juga mencatat daftar plugin transformasi yang digunakan:

Daftar plugin yang digunakan

Daftarnya cukup panjang! Ini adalah semua plugin yang perlu digunakan Babel untuk mengubah sintaksis ES2015+ ke sintaksis lama untuk semua browser yang ditargetkan.

Namun, Babel tidak menampilkan polyfill apa pun yang digunakan:

Tidak ada polyfill yang ditambahkan

Hal ini karena seluruh @babel/polyfill diimpor secara langsung.

Memuat polyfill satu per satu

Secara default, Babel menyertakan setiap polyfill yang diperlukan untuk lingkungan ES2015+ lengkap saat @babel/polyfill diimpor ke dalam file. Untuk mengimpor polyfill tertentu yang diperlukan untuk browser target, tambahkan useBuiltIns: 'entry' ke konfigurasi.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true
        "useBuiltIns": "entry"
      }
    ]
  ]
}

Muat ulang aplikasi. Sekarang Anda dapat melihat semua polyfill spesifik yang disertakan:

Daftar polyfill yang diimpor

Meskipun sekarang hanya polyfill yang diperlukan untuk "last 2 versions" yang disertakan, daftar ini masih merupakan daftar yang sangat panjang. Ini karena polyfill yang diperlukan untuk browser target untuk setiap fitur yang lebih baru tetap disertakan. Ubah nilai atribut menjadi usage agar hanya menyertakan yang diperlukan untuk fitur yang sedang digunakan dalam kode.

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "debug": true,
        "useBuiltIns": "entry"
        "useBuiltIns": "usage"
      }
    ]
  ]
}

Dengan cara ini, polyfill secara otomatis disertakan jika diperlukan. Ini berarti Anda dapat menghapus impor @babel/polyfill di src/index.js.

import "./style.css";
import "@babel/polyfill";

Sekarang hanya polyfill yang diperlukan untuk aplikasi yang disertakan.

Daftar polyfill secara otomatis disertakan

Ukuran paket aplikasi berkurang secara signifikan.

Ukuran paket dikurangi menjadi 30,1 KB

Mempersempit daftar browser yang didukung

Jumlah target browser yang disertakan masih cukup besar, dan tidak banyak pengguna yang menggunakan browser yang dihentikan seperti Internet Explorer. Perbarui konfigurasi menjadi berikut ini:

{
  "presets": [
    [
      "@babel/preset-env",
      {
        "targets": "last 2 versions",
        "targets": [">0.25%", "not ie 11"],
        "debug": true,
        "useBuiltIns": "usage",
      }
    ]
  ]
}

Lihat detail paket yang diambil.

Ukuran paket 30,0 KB

Karena aplikasinya sangat kecil, tidak ada banyak perbedaan dengan perubahan ini. Namun, penggunaan persentase pangsa pasar browser (seperti ">0.25%") beserta mengecualikan browser tertentu yang Anda yakin tidak digunakan adalah pendekatan yang direkomendasikan. Lihat artikel "2 versi terakhir" yang dianggap berbahaya oleh James Kyle untuk mempelajari lebih lanjut tentang hal ini.

Gunakan <script type="module">

Masih ada banyak yang harus ditingkatkan. Meskipun sejumlah polyfill yang tidak terpakai telah dihapus, ada banyak polyfill yang dikirim yang tidak diperlukan untuk beberapa browser. Dengan menggunakan modul, sintaksis yang lebih baru dapat ditulis dan dikirimkan ke browser secara langsung tanpa menggunakan polyfill yang tidak perlu.

Modul JavaScript adalah fitur yang relatif baru yang didukung di semua browser utama. Modul dapat dibuat menggunakan atribut type="module" untuk menentukan skrip yang diimpor dan diekspor dari modul lain. Contoh:

// math.mjs
export const add = (x, y) => x + y;

<!-- index.html -->
<script type="module">
  import { add } from './math.mjs';

  add(5, 2); // 7
</script>

Banyak fitur ECMAScript baru yang sudah didukung di lingkungan yang mendukung modul JavaScript (bukan menggunakan Babel.) Hal ini berarti konfigurasi Babel dapat dimodifikasi untuk mengirim dua versi aplikasi yang berbeda ke browser:

  • Versi yang akan berfungsi di browser baru yang mendukung modul dan mencakup modul yang sebagian besar tidak ditranspilasi tetapi memiliki ukuran file yang lebih kecil
  • Versi yang menyertakan skrip yang lebih besar dan ditranspilasi yang akan berfungsi pada browser lama

Menggunakan Modul ES dengan Babel

Untuk memiliki setelan @babel/preset-env terpisah untuk dua versi aplikasi, hapus file .babelrc. Setelan Babel dapat ditambahkan ke konfigurasi webpack dengan menentukan dua format kompilasi yang berbeda untuk setiap versi aplikasi.

Mulai dengan menambahkan konfigurasi untuk skrip lama ke webpack.config.js:

const legacyConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].bundle.js"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: false
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Perhatikan bahwa alih-alih menggunakan nilai targets untuk "@babel/preset-env", esmodules dengan nilai false digunakan sebagai gantinya. Ini berarti Babel menyertakan semua transformasi dan polyfill yang diperlukan untuk menargetkan setiap browser yang belum mendukung modul ES.

Tambahkan objek entry, cssRule, dan corePlugins ke awal file webpack.config.js. Semuanya digunakan bersama antara modul dan skrip lama yang disalurkan ke browser.

const entry = {
  main: "./src"
};

const cssRule = {
  test: /\.css$/,
  use: ExtractTextPlugin.extract({
    fallback: "style-loader",
    use: "css-loader"
  })
};

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"})
];

Demikian pula, buat objek konfigurasi untuk skrip modul di bawah tempat legacyConfig ditentukan:

const moduleConfig = {
  entry,
  output: {
    path: path.resolve(__dirname, "public"),
    filename: "[name].mjs"
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: "babel-loader",
        options: {
          presets: [
            ["@babel/preset-env", {
              useBuiltIns: "usage",
              targets: {
                esmodules: true
              }
            }]
          ]
        }
      },
      cssRule
    ]
  },
  plugins
}

Perbedaan utama di sini adalah ekstensi file .mjs digunakan untuk nama file output. Nilai esmodules disetel ke benar (true) yang berarti kode yang dihasilkan ke modul ini adalah skrip yang lebih kecil dan kurang dikompilasi yang tidak melalui transformasi apa pun dalam contoh ini karena semua fitur yang digunakan sudah didukung di browser yang mendukung modul.

Di akhir file tersebut, ekspor kedua konfigurasi dalam satu array.

module.exports = [
  legacyConfig, moduleConfig
];

Sekarang, hal ini akan membuat modul yang lebih kecil untuk browser yang mendukungnya dan skrip yang ditranspilasi lebih besar untuk browser lama.

Browser yang mendukung modul mengabaikan skrip dengan atribut nomodule. Sebaliknya, browser yang tidak mendukung modul akan mengabaikan elemen skrip dengan type="module". Artinya, Anda dapat menyertakan modul serta penggantian yang dikompilasi. Idealnya, dua versi aplikasi harus berada di index.html seperti ini:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js"></script>

Browser yang mendukung modul mengambil dan menjalankan main.mjs serta mengabaikan main.bundle.js. Browser yang tidak mendukung modul akan melakukan hal yang sebaliknya.

Penting untuk diperhatikan bahwa tidak seperti skrip reguler, skrip modul selalu ditangguhkan secara default. Jika Anda ingin skrip nomodule yang setara juga ditangguhkan dan hanya dieksekusi setelah diurai, Anda harus menambahkan atribut defer:

<script type="module" src="main.mjs"></script>
<script nomodule src="main.bundle.js" defer></script>

Hal terakhir yang perlu dilakukan di sini adalah menambahkan masing-masing atribut module dan nomodule ke modul dan skrip lama. Impor ScriptExtHtmlWebpackPlugin di bagian paling atas webpack.config.js:

const path = require("path");

const webpack = require("webpack");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ScriptExtHtmlWebpackPlugin = require("script-ext-html-webpack-plugin");

Sekarang perbarui array plugins dalam konfigurasi untuk menyertakan plugin ini:

const plugins = [
  new ExtractTextPlugin({filename: "[name].css", allChunks: true}),
  new HtmlWebpackPlugin({template: "./src/index.html"}),
  new ScriptExtHtmlWebpackPlugin({
    module: /\.mjs$/,
    custom: [
      {
        test: /\.js$/,
        attribute: 'nomodule',
        value: ''
    },
    ]
  })
];

Setelan plugin ini menambahkan atribut type="module" untuk semua elemen skrip .mjs serta atribut nomodule untuk semua modul skrip .js.

Modul penayangan dalam dokumen HTML

Hal terakhir yang perlu dilakukan adalah membuat output elemen skrip lama dan modern ke file HTML. Sayangnya, plugin yang membuat file HTML akhir, HTMLWebpackPlugin, saat ini tidak mendukung output skrip modul dan nomodule. Meskipun ada solusi dan plugin terpisah yang dibuat untuk mengatasi masalah ini, seperti BabelMultiTargetPlugin dan HTMLWebpackMultiBuildPlugin, pendekatan yang lebih sederhana untuk menambahkan elemen skrip modul secara manual digunakan untuk tujuan tutorial ini.

Tambahkan kode berikut ke src/index.js di akhir file:

    ...
    </form>
    <script type="module" src="main.mjs"></script>
  </body>
</html>

Sekarang, muat aplikasi di browser yang mendukung modul, seperti Chrome versi terbaru.

Modul 5,2 KB diambil melalui jaringan untuk browser yang lebih baru

Hanya modul yang diambil, dengan ukuran paket yang jauh lebih kecil karena sebagian besar tidak ditranspilasi. Elemen skrip lainnya sepenuhnya diabaikan oleh browser.

Jika Anda memuat aplikasi di browser lama, hanya skrip yang lebih besar dan telah ditranspilasi dengan semua polyfill dan transformasi yang diperlukan yang akan diambil. Berikut adalah screenshot untuk semua permintaan yang dibuat di Chrome versi lama (versi 38).

Skrip sebesar 30 KB diambil untuk browser lama

Kesimpulan

Anda kini memahami cara menggunakan @babel/preset-env untuk hanya menyediakan polyfill yang diperlukan untuk browser yang ditargetkan. Anda juga sudah mengetahui cara modul JavaScript dapat meningkatkan performa lebih lanjut dengan mengirimkan dua versi aplikasi yang ditranspilasi dan berbeda. Dengan pemahaman yang baik tentang bagaimana kedua teknik ini dapat mengurangi ukuran paket Anda secara signifikan, lanjutkan dan optimalkan.