Teknik agar aplikasi web dimuat dengan cepat, bahkan di ponsel menengah

Cara kami menggunakan pemisahan kode, penyisipan kode, dan rendering sisi server di PROXX.

Di Google I/O 2019 Mariko, Jake, dan saya mengirimkan PROXX, clone Minesweeper modern untuk web. Sesuatu yang membedakan PROXX adalah fokus pada aksesibilitas (Anda dapat memainkannya dengan pembaca layar!) dan kemampuan untuk berjalan dengan baik di ponsel menengah seperti pada perangkat desktop kelas atas. Ponsel menengah dibatasi dalam beberapa hal:

  • CPU Lemah
  • GPU yang lemah atau tidak ada
  • Layar kecil tanpa input sentuh
  • Jumlah memori yang sangat terbatas

Namun, perangkat ini menjalankan browser modern dan sangat terjangkau. Oleh karena itu, ponsel menengah semakin populer di pasar negara berkembang. Titik harga produk tersebut memungkinkan audiens baru, yang sebelumnya tidak mampu membayarnya, untuk terhubung secara online dan memanfaatkan web modern. Untuk tahun 2019, diperkirakan sekitar 400 juta ponsel menengah akan dijual di India, sehingga pengguna ponsel menengah mungkin menjadi bagian penting dari audiens Anda. Selain itu, kecepatan koneksi yang mirip dengan 2G merupakan hal yang lumrah di pasar negara berkembang. Bagaimana cara kami membuat PROXX berfungsi dengan baik dalam kondisi ponsel menengah?

Gameplay PROXX.

Performa itu penting, dan itu mencakup performa pemuatan dan performa runtime. Telah terbukti bahwa performa yang baik berkorelasi dengan peningkatan retensi pengguna, peningkatan konversi, dan—yang terpenting—peningkatan inklusivitas. Jeremy Wagner memiliki lebih banyak data dan insight tentang pentingnya performa.

Artikel ini adalah bagian 1 dari seri yang terdiri dari dua bagian. Bagian 1 berfokus pada performa pemuatan dan bagian 2 akan berfokus pada performa runtime.

Memahami {i>status quo<i}

Menguji performa pemuatan Anda di perangkat sebenarnya sangatlah penting. Jika Anda tidak memiliki perangkat sungguhan, sebaiknya gunakan WebPageTest, khususnya penyiapan "sederhana". WPT menjalankan baterai pengujian pemuatan pada perangkat sebenarnya dengan koneksi 3G yang diemulasikan.

3G adalah kecepatan yang baik untuk mengukur. Meskipun Anda mungkin sudah terbiasa dengan 4G, LTE, atau bahkan 5G, kenyataannya internet seluler terlihat sangat berbeda. Mungkin Anda sedang di kereta, di konferensi, di konser, atau di pesawat. Apa yang akan Anda alami di sana kemungkinan besar mendekati 3G, dan terkadang bahkan lebih buruk.

Jadi, kami akan fokus pada 2G dalam artikel ini karena PROXX secara eksplisit menargetkan ponsel menengah dan pasar negara berkembang sebagai target audiensnya. Setelah WebPageTest menjalankan pengujiannya, Anda akan mendapatkan waterfall (serupa dengan yang Anda lihat di DevTools) serta setrip film di bagian atas. Strip film menampilkan apa yang dilihat pengguna saat aplikasi Anda dimuat. Pada 2G, pengalaman pemuatan versi PROXX yang tidak dioptimalkan cukup buruk:

Video setrip film menunjukkan apa yang dilihat pengguna saat PROXX dimuat pada perangkat kelas bawah yang sebenarnya melalui koneksi 2G yang diemulasikan.

Saat dimuat melalui 3G, pengguna akan melihat kosong selama 4 detik. Di jaringan 2G, pengguna sama sekali tidak melihat apa pun selama lebih dari 8 detik. Jika Anda telah membaca mengapa performa penting, Anda tahu bahwa kami telah kehilangan sebagian besar calon pengguna karena ketidaksabaran. Pengguna perlu mendownload seluruh 62 KB JavaScript agar apa pun dapat muncul di layar. Lapisan peraknya dalam skenario ini adalah bahwa hal kedua yang muncul di layar itu juga interaktif. Tapi, apa benar begitu?

[First Artiful Paint][FMP] dalam versi PROXX yang tidak dioptimalkan secara _teknis_ [interaktif][TTI], tetapi tidak berguna bagi pengguna.

Setelah sekitar 62 KB JS dengan gzip didownload dan DOM dibuat, pengguna dapat melihat aplikasi kita. Aplikasi ini secara teknis interaktif. Namun, melihat visualnya menunjukkan kenyataan yang berbeda. Font web masih dimuat di latar belakang dan pengguna tidak dapat melihat teks sampai font tersebut siap. Meskipun memenuhi syarat sebagai First Artiful Paint (FMP), status ini pasti tidak memenuhi syarat sebagai interaktif yang tepat, karena pengguna tidak dapat mengetahui apa dari input tersebut. Diperlukan satu detik lagi di jaringan 3G dan 3 detik di jaringan 2G hingga aplikasi siap digunakan. Secara keseluruhan, aplikasi ini membutuhkan waktu 6 detik di 3G dan 11 detik di 2G untuk menjadi interaktif.

Analisis {i>Waterfall<i}

Setelah mengetahui apa yang dilihat pengguna, kita perlu mengetahui alasannya. Untuk itu, kita dapat melihat waterfall dan menganalisis alasan resource terlambat dimuat. Dalam jejak 2G untuk PROXX, kita dapat melihat dua bendera merah utama:

  1. Ada beberapa garis tipis warna-warni.
  2. File JavaScript membentuk rantai. Misalnya, resource kedua hanya mulai dimuat setelah resource pertama selesai, dan resource ketiga hanya dimulai saat resource kedua selesai.
Waterfall ini memberikan insight tentang resource mana yang dimuat, kapan waktunya, dan berapa lama waktu pemuatannya.

Mengurangi jumlah koneksi

Setiap garis tipis (dns, connect, ssl) menunjukkan pembuatan koneksi HTTP baru. Menyiapkan koneksi baru mahal karena membutuhkan waktu sekitar 1 pada 3G dan sekitar 2,5 detik pada 2G. Di waterfall, kita melihat koneksi baru untuk:

  • Permintaan #1: index.html kami
  • Permintaan #5: Gaya font dari fonts.googleapis.com
  • Permintaan #8: Google Analytics
  • Permintaan #9: File font dari fonts.gstatic.com
  • Permintaan #14: Manifes aplikasi web

Koneksi baru untuk index.html tidak dapat dihindari. Browser harus membuat koneksi ke server untuk mendapatkan konten. Koneksi baru untuk Google Analytics dapat dihindari dengan membuat konten seperti Analytics Minimal menjadi inline. Namun, Google Analytics tidak memblokir aplikasi kami untuk melakukan rendering atau menjadi interaktif, jadi kami tidak terlalu peduli dengan seberapa cepat aplikasi dimuat. Idealnya, Google Analytics harus dimuat dalam waktu tidak ada aktivitas, saat semua hal lainnya telah dimuat. Dengan begitu, arsitektur itu tidak akan mengambil {i>bandwidth<i} atau daya pemrosesan selama pemuatan awal. Koneksi baru untuk manifes aplikasi web ditentukan oleh spesifikasi pengambilan, karena manifes harus dimuat melalui koneksi yang tidak memiliki kredensial. Sekali lagi, manifes aplikasi web tidak memblokir aplikasi untuk melakukan rendering atau menjadi interaktif, jadi kita tidak perlu terlalu peduli.

Namun, kedua font dan gayanya menjadi masalah karena memblokir rendering dan juga interaktivitas. Jika kita melihat CSS yang ditampilkan oleh fonts.googleapis.com, itu hanya dua aturan @font-face, satu untuk setiap font. Gaya font sebenarnya sangat kecil sehingga kami memutuskan untuk menyisipkannya ke dalam HTML kami, menghapus satu koneksi yang tidak perlu. Untuk menghindari biaya penyiapan koneksi bagi file font, kita dapat menyalinnya ke server kita sendiri.

Paralelkan beban

Dengan melihat waterfall, kita dapat melihat bahwa setelah file JavaScript pertama selesai dimuat, file baru akan segera dimuat. Hal ini umum untuk dependensi modul. Modul utama kita mungkin memiliki impor statis, sehingga JavaScript tidak dapat berjalan sampai impor tersebut dimuat. Hal penting yang perlu disadari di sini adalah bahwa jenis dependensi ini diketahui pada waktu build. Kita dapat menggunakan tag <link rel="preload"> untuk memastikan semua dependensi mulai dimuat begitu kita menerima HTML.

Hasil

Mari kita lihat apa yang telah dicapai perubahan kita. Penting untuk tidak mengubah variabel lain dalam penyiapan pengujian kita yang dapat mendistorsi hasil, jadi kita akan menggunakan penyiapan sederhana WebPageTest untuk sisa artikel ini dan melihat setrip film:

Kami menggunakan setrip film WebPageTest untuk melihat pencapaian perubahan kami.

Perubahan ini mengurangi TTI dari 11 menjadi 8,5, yang merupakan kira-kira 2,5 detik waktu penyiapan koneksi yang ingin kami hapus. Bagus.

Pra-rendering

Meskipun kami baru saja mengurangi TTI, kami tidak benar-benar memengaruhi layar putih yang lama dan harus bertahan selama 8,5 detik. Dapat dikatakan bahwa peningkatan terbesar untuk FMP dapat dicapai dengan mengirimkan markup bergaya di index.html. Teknik umum untuk mencapai hal ini adalah pra-rendering dan rendering sisi server, yang terkait erat dan dijelaskan dalam Rendering di Web. Kedua teknik ini menjalankan aplikasi web di Node dan membuat serialisasi DOM yang dihasilkan ke HTML. Rendering sisi server melakukan hal ini per permintaan di sisi server, sementara pra-rendering melakukannya pada waktu build dan menyimpan output sebagai index.html baru Anda. Karena PROXX adalah aplikasi JAMStack dan tidak memiliki sisi server, kami memutuskan untuk menerapkan pra-rendering.

Ada banyak cara untuk mengimplementasikan pra-rendering. Di PROXX, kami memilih untuk menggunakan Puppeteer, yang memulai Chrome tanpa UI apa pun dan memungkinkan Anda mengontrol instance tersebut dari jarak jauh dengan Node API. Kita menggunakan ini untuk memasukkan markup dan JavaScript, lalu membaca kembali DOM sebagai string HTML. Karena menggunakan Modul CSS, kita mendapatkan CSS yang disisipkan dalam gaya yang kita butuhkan secara gratis.

  const browser = await puppeteer.launch();
  const page = await browser.newPage();
  await page.setContent(rawIndexHTML);
  await page.evaluate(codeToRun);
  const renderedHTML = await page.content();
  browser.close();
  await writeFile("index.html", renderedHTML);

Dengan menerapkan kebijakan ini, kami dapat mengharapkan peningkatan untuk FMP kami. Kita masih harus memuat dan mengeksekusi jumlah JavaScript yang sama seperti sebelumnya, jadi kita tidak akan berharap TTI akan banyak berubah. Jika ada, index.html kita telah semakin besar dan mungkin akan sedikit menahan TTI. Hanya ada satu cara untuk mengetahuinya: menjalankan WebPageTest.

Strip film menunjukkan peningkatan yang jelas untuk metrik FMP. TTI sebagian besar tidak terpengaruh.

First Artifactful Paint kami meningkat dari 8,5 detik menjadi 4,9 detik, sebuah peningkatan besar. TTI kami masih terjadi sekitar 8,5 detik sehingga sebagian besar tidak terpengaruh oleh perubahan ini. Apa yang kami lakukan di sini adalah perubahan persepsi. Beberapa orang bahkan mungkin menyebutnya sebagai sulap. Dengan merender visual menengah game, kami mengubah performa pemuatan yang dirasakan menjadi lebih baik.

{i>Inline<i}

Metrik lain yang diberikan DevTools dan WebPageTest adalah Time To First Byte (TTFB). Ini adalah waktu yang diperlukan dari byte pertama permintaan yang dikirim ke byte pertama respons yang diterima. Waktu ini juga sering disebut Waktu Round Trip (RTT), meskipun secara teknis ada perbedaan antara kedua angka ini: RTT tidak termasuk waktu pemrosesan permintaan di sisi server. DevTools dan WebPageTest memvisualisasikan TTFB dengan warna terang dalam blok permintaan/respons.

Bagian terang dari permintaan menandakan bahwa permintaan sedang menunggu untuk menerima byte pertama respons.

Dengan melihat waterfall, kita dapat melihat bahwa semua permintaan menghabiskan sebagian besar waktu mereka untuk menunggu byte pertama respons tiba.

Masalah ini adalah tujuan awal dari HTTP/2 Push. Developer aplikasi mengetahui bahwa resource tertentu diperlukan dan dapat mendorong resource tersebut. Saat klien menyadari bahwa perlu mengambil sumber daya tambahan, sumber daya tersebut sudah ada di cache browser. HTTP/2 Push ternyata terlalu sulit untuk disetel dengan benar dan dianggap tidak disarankan. Ruang masalah ini akan ditinjau kembali selama standardisasi HTTP/3. Untuk saat ini, solusi termudah adalah menyejajarkan semua resource penting dengan mengorbankan efisiensi dalam cache.

CSS penting kita sudah menjadi inline berkat Modul CSS dan pra-rendering berbasis Puppeteer. Untuk JavaScript, kita perlu menyejajarkan modul penting kita dan dependensinya. Tugas ini memiliki tingkat kesulitan yang berbeda-beda, berdasarkan pemaket yang Anda gunakan.

Dengan inline JavaScript, kami telah mengurangi TTI dari 8,5 dtk menjadi 7,2 dtk.

Ini mencukur 1 detik dari TTI kami. Sekarang kita telah mencapai titik di mana index.html berisi semua yang diperlukan untuk render awal dan menjadi interaktif. HTML dapat dirender saat sedang didownload, sehingga menghasilkan FMP. Saat HTML selesai menguraikan dan dieksekusi, aplikasi akan menjadi interaktif.

Pemisahan kode yang agresif

Ya, index.html kami berisi semua yang diperlukan untuk menjadi interaktif. Namun, setelah diperiksa lebih dekat, data itu juga berisi hal-hal lainnya. index.html kami berukuran sekitar 43 KB. Mari kita masukkan hal tersebut sehubungan dengan apa yang dapat berinteraksi dengan pengguna di awal: Kita memiliki formulir untuk mengonfigurasi game yang berisi beberapa komponen, tombol mulai, dan mungkin beberapa kode untuk dipertahankan dan memuat setelan pengguna. Cukup begitu saja. 43 KB sepertinya banyak.

Halaman landing PROXX. Hanya komponen penting yang digunakan di sini.

Untuk memahami asal ukuran paket, kita dapat menggunakan penjelajah peta sumber atau alat serupa untuk memerinci isi paket. Seperti yang diprediksi, paket kita berisi logika game, mesin rendering, layar menang, layar hilang, dan berbagai utilitas. Hanya sebagian kecil dari modul ini yang diperlukan untuk halaman landing. Memindahkan semua yang tidak benar-benar diperlukan untuk interaktivitas ke dalam modul yang dimuat secara lambat akan mengurangi TTI secara signifikan.

Menganalisis konten `index.html` PROXX akan menunjukkan banyak resource yang tidak dibutuhkan. Resource penting ditandai.

Yang perlu kita lakukan adalah pemisahan kode. Pemisahan kode memecah paket monolitik menjadi bagian-bagian lebih kecil yang dapat dimuat dengan lambat sesuai permintaan. Pemaket populer seperti Webpack, Rollup, dan Parcel mendukung pemisahan kode dengan menggunakan import() dinamis. Pemaket akan menganalisis kode Anda dan melakukan inline pada semua modul yang diimpor secara statis. Semua yang Anda impor secara dinamis akan dimasukkan ke dalam filenya sendiri dan hanya akan diambil dari jaringan setelah panggilan import() dieksekusi. Tentu saja, menjangkau jaringan memerlukan biaya dan hanya boleh dilakukan jika Anda memiliki waktu luang. Yang terpenting di sini adalah mengimpor modul yang sangat diperlukan pada waktu pemuatan dan secara dinamis memuat modul lainnya. Namun, sebaiknya jangan menunggu hingga saat terakhir untuk menjalankan lazy-load modul yang pasti akan digunakan. Idle Getting Urgent dari Phil Walton adalah pola yang bagus untuk memberikan jalan tengah yang baik antara pemuatan lambat dan pemuatan cepat.

Di PROXX, kita membuat file lazy.js yang secara statis mengimpor semua yang tidak dibutuhkan. Di file utama, kita kemudian dapat mengimpor lazy.js secara dinamis. Namun, beberapa komponen Preact berakhir di lazy.js, yang ternyata sedikit detail karena Preact tidak dapat langsung menangani komponen yang dimuat dengan lambat. Oleh karena itu, kami menulis wrapper komponen deferred kecil yang memungkinkan kita merender placeholder hingga komponen sebenarnya dimuat.

export default function deferred(componentPromise) {
  return class Deferred extends Component {
    constructor(props) {
      super(props);
      this.state = {
        LoadedComponent: undefined
      };
      componentPromise.then(component => {
        this.setState({ LoadedComponent: component });
      });
    }

    render({ loaded, loading }, { LoadedComponent }) {
      if (LoadedComponent) {
        return loaded(LoadedComponent);
      }
      return loading();
    }
  };
}

Dengan menerapkan ini, kita dapat menggunakan Promise komponen dalam fungsi render(). Misalnya, komponen <Nebula>, yang merender gambar latar animasi, akan diganti dengan <div> kosong saat komponen dimuat. Setelah komponen dimuat dan siap digunakan, <div> akan diganti dengan komponen sebenarnya.

const NebulaDeferred = deferred(
  import("/components/nebula").then(m => m.default)
);

return (
  // ...
  <NebulaDeferred
    loading={() => <div />}
    loaded={Nebula => <Nebula />}
  />
);

Dengan semua ini, kami mengurangi index.html menjadi hanya 20 KB, kurang dari setengah dari ukuran aslinya. Apa efeknya terhadap FMP dan TTI? WebPageTest akan memberitahunya!

Strip film mengonfirmasi: TTI kita sekarang di 5,4 dtk. Peningkatan drastis dari 11 versi awal kami.

FMP dan TTI hanya berjauhan 100 md, karena ini hanya masalah penguraian dan eksekusi JavaScript yang disisipkan. Setelah hanya 5,4 detik di 2G, aplikasi ini benar-benar interaktif. Semua modul lainnya yang kurang penting dimuat di latar belakang.

Lebih Banyak Kebingungan

Jika Anda melihat daftar modul penting kami di atas, Anda akan melihat bahwa mesin rendering bukan bagian dari modul penting. Tentu saja, game tidak dapat dimulai sampai kita memiliki mesin rendering untuk merender game. Kita dapat menonaktifkan tombol "Start" sampai mesin rendering siap untuk memulai game, tetapi menurut pengalaman kita, pengguna biasanya membutuhkan waktu cukup lama untuk mengonfigurasi setelan game sehingga hal ini tidak diperlukan. Biasanya, mesin rendering dan modul lainnya selesai memuat saat pengguna menekan "Start". Pada kasus yang jarang terjadi ketika pengguna lebih cepat daripada koneksi jaringannya, kami menampilkan layar pemuatan sederhana yang menunggu modul yang tersisa selesai.

Kesimpulan

Pengukuran itu penting. Untuk menghindari menghabiskan waktu pada masalah yang tidak nyata, sebaiknya selalu ukur terlebih dahulu sebelum mengimplementasikan pengoptimalan. Selain itu, pengukuran harus dilakukan pada perangkat sebenarnya dengan koneksi 3G atau di WebPageTest jika tidak ada perangkat sungguhan yang digunakan.

Strip film dapat memberikan insight tentang tanggapan pemuatan aplikasi Anda bagi pengguna. Waterfall dapat memberi tahu Anda resource apa yang menyebabkan waktu pemuatan yang mungkin lama. Berikut adalah checklist hal-hal yang dapat Anda lakukan untuk meningkatkan performa pemuatan:

  • Kirim aset sebanyak mungkin melalui satu koneksi.
  • Pramuat atau bahkan resource inline yang diperlukan untuk render dan interaktivitas pertama.
  • Melakukan pra-rendering aplikasi untuk meningkatkan performa pemuatan yang dirasakan.
  • Menggunakan pemisahan kode secara agresif untuk mengurangi jumlah kode yang diperlukan untuk interaktivitas.

Nantikan bagian 2 yang membahas cara mengoptimalkan performa runtime di perangkat yang sangat terbatas.