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. Yang membedakan PROXX adalah fokus pada aksesibilitas (dapat Anda mainkan dengan pembaca layar) dan kemampuan untuk berjalan dengan baik di ponsel menengah seperti di perangkat desktop kelas atas. Ponsel menengah dibatasi dalam beberapa cara:

  • CPU lemah
  • GPU yang lemah atau tidak ada
  • Layar kecil tanpa input sentuh
  • Memori yang sangat terbatas

Namun, Chromebook menjalankan browser modern dan harganya sangat terjangkau. Karena alasan ini, ponsel menengah muncul kembali di pasar negara berkembang. Titik harga produk ini memungkinkan audiens baru, yang sebelumnya tidak mampu membayarnya, untuk mengakses internet dan menggunakan web modern. Untuk tahun 2019, diperkirakan sekitar 400 juta ponsel menengah akan dijual di India saja, sehingga pengguna ponsel menengah dapat menjadi sebagian besar dari jumlah audiens Anda. Selain itu, kecepatan koneksi yang mirip dengan 2G adalah norma di pasar negara berkembang. Bagaimana kami bisa 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 berhubungan dengan peningkatan retensi pengguna, peningkatan konversi, dan, yang paling penting, peningkatan inklusivitas. Jeremy Wagner memiliki lebih banyak data dan insight tentang alasan pentingnya performa.

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

Memahami {i>status quo<i}

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

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

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

Video setrip film menunjukkan apa yang dilihat pengguna saat PROXX dimuat di perangkat low-end yang sebenarnya melalui koneksi 2G yang diemulasikan.

Saat dimuat melalui 3G, pengguna akan melihat tidak ada apa-apa selama 4 detik. Dengan jaringan 2G, pengguna sama sekali tidak melihat apa pun selama lebih dari 8 detik. Jika Anda membaca mengapa performa penting, Anda tahu bahwa sekarang kami kehilangan sebagian besar calon pengguna karena ketidaksabaran. Pengguna harus mendownload semua 62 KB JavaScript agar apa pun dapat muncul di layar. Sisi positifnya dalam skenario ini adalah bahwa hal kedua yang muncul di layar juga merupakan hal 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 gzip didownload dan DOM dibuat, pengguna dapat melihat aplikasi kita. Aplikasi ini secara teknis interaktif. Namun, melihat visualisasinya menunjukkan realitas yang berbeda. Font web masih dimuat di latar belakang dan hingga siap, pengguna tidak dapat melihat teks. Meskipun memenuhi syarat sebagai First Artiful Paint (FMP), status ini tentu saja tidak memenuhi syarat sebagai interaktif yang benar, karena pengguna tidak dapat mengetahui tentang input apa pun. Diperlukan waktu satu detik pada 3G dan 3 detik pada 2G sampai aplikasi siap digunakan. Secara keseluruhan, aplikasi ini memerlukan waktu 6 detik pada 3G dan 11 detik pada 2G untuk menjadi interaktif.

Analisis {i>Waterfall<i}

Sekarang kita tahu apa yang dilihat pengguna, kita perlu mencari tahu mengapa. Untuk itu, kita dapat melihat waterfall dan menganalisis penyebab resource terlambat dimuat. Dalam pelacakan 2G PROXX, kita dapat melihat dua penanda penting utama:

  1. Ada beberapa garis tipis yang berwarna-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 dan berapa lama waktu yang dibutuhkan.

Mengurangi jumlah koneksi

Setiap baris tipis (dns, connect, ssl) menunjukkan pembuatan koneksi HTTP baru. Menyiapkan koneksi baru membutuhkan biaya sekitar 1 detik 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 menyisipkan sesuatu seperti Minimal Analytics, tetapi Google Analytics tidak memblokir aplikasi kami untuk melakukan rendering atau menjadi interaktif, sehingga kami tidak terlalu peduli dengan kecepatan pemuatannya. Idealnya, Google Analytics harus dimuat dalam waktu tidak ada aktivitas, ketika semua hal lainnya sudah dimuat. Dengan begitu, komponen itu tidak akan memakan {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 tanpa kredensial. Sekali lagi, manifes aplikasi web tidak memblokir aplikasi kita dari rendering atau menjadi interaktif, sehingga kita tidak perlu terlalu peduli.

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

Memparalelkan beban

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

Hasil

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

Kami menggunakan setrip film WebPageTest untuk melihat hasil perubahan.

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

Pra-rendering

Meskipun kita baru saja mengurangi TTI, kita tidak benar-benar memengaruhi layar putih yang sangat panjang yang harus bertahan selama 8,5 detik. Bisa dibilang 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 tersebut akan menjalankan aplikasi web di Node dan melakukan 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. Karena PROXX adalah aplikasi JAMStack dan tidak memiliki sisi server, kami memutuskan untuk menerapkan pra-rendering.

Ada banyak cara untuk menerapkan 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 menggunakannya untuk memasukkan markup dan JavaScript kemudian membaca kembali DOM sebagai string HTML. Karena kita menggunakan Modul CSS, kita mendapatkan CSS sebaris gaya yang dibutuhkan 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 hal ini, kami dapat mengharapkan peningkatan untuk FMP kami. Kita masih perlu memuat dan mengeksekusi jumlah JavaScript yang sama seperti sebelumnya, jadi seharusnya TTI tidak akan banyak berubah. Jika ada, index.html telah menjadi lebih besar dan mungkin sedikit memundurkan TTI. Hanya ada satu cara untuk mengetahuinya: menjalankan WebPageTest.

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

Gambar Pertama Kami yang Bermakna telah berubah dari 8,5 detik menjadi 4,9 detik, peningkatan yang besar. TTI kita masih terjadi sekitar 8,5 detik sehingga sebagian besar tidak terpengaruh oleh perubahan ini. Apa yang kami lakukan di sini adalah perubahan persepsi. Beberapa 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 sejak byte pertama permintaan dikirim ke byte pertama dari respons yang diterima. Waktu ini juga sering disebut Waktu Round Trip (RTT), meskipun secara teknis ada perbedaan di antara kedua angka ini: RTT tidak mencakup waktu pemrosesan permintaan di sisi server. DevTools dan WebPageTest memvisualisasikan TTFB dengan warna terang dalam blok permintaan/respons.

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

Dengan melihat waterfall, kita dapat melihat bahwa semua permintaan menghabiskan sebagian besar waktunya menunggu hingga byte pertama respons diterima.

Masalah inilah yang awalnya disusun untuk HTTP/2 Push. Developer aplikasi mengetahui bahwa resource tertentu diperlukan dan dapat mendorong resource tersebut. Pada saat klien menyadari bahwa perlu mengambil resource tambahan, resource tersebut sudah ada di cache browser. HTTP/2 Push ternyata terlalu sulit untuk diperbaiki 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 caching.

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

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

Ini mengurangi 1 detik TTI kami. Kita sekarang telah mencapai titik di mana index.html berisi semua yang diperlukan untuk render awal dan menjadi interaktif. HTML dapat dirender saat masih didownload, sehingga menghasilkan FMP. Saat HTML selesai mengurai dan mengeksekusi, aplikasi akan interaktif.

Pemisahan kode yang agresif

Ya, index.html berisi semua yang diperlukan untuk menjadi interaktif. Tapi setelah diperiksa, ternyata isinya juga berisi hal-hal lain. index.html kami 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 mempertahankan dan memuat setelan pengguna. Cukup begitu saja. 43 KB itu banyak sekali.

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

Untuk memahami asal ukuran paket, kami dapat menggunakan penjelajah peta sumber atau alat serupa untuk menguraikan paket yang disertakan. Seperti yang diprediksi, paket kita berisi logika game, mesin rendering, layar menang, layar kalah, dan banyak 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 dengan lambat akan mengurangi TTI secara signifikan.

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

Yang perlu kita lakukan adalah pemisahan kode. Pemisahan kode memecah paket monolitik menjadi bagian-bagian lebih kecil yang dapat dimuat secara lambat sesuai permintaan. Pemaket populer seperti Webpack, Rollup, dan Parcel mendukung pemisahan kode dengan menggunakan import() dinamis. Pemaket akan menganalisis kode Anda dan inline 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() dijalankan. Tentu saja menghabiskan biaya dan hanya bisa dilakukan jika Anda memiliki waktu luang. Mantranya adalah mengimpor modul yang sangat dibutuhkan pada waktu pemuatan secara statis dan memuat modul lainnya secara dinamis. Namun, Anda tidak perlu menunggu hingga saat terakhir untuk melakukan lazy-load modul yang pasti akan digunakan. Idle Get Urgent dari Phil Walton adalah pola yang bagus untuk mendapatkan jalan tengah yang sehat antara pemuatan lambat dan pemuatan cepat.

Di PROXX, kita membuat file lazy.js yang secara statis mengimpor semua yang tidak diperlukan. Di file utama, kita kemudian dapat mengimpor lazy.js secara dinamis. Namun, beberapa komponen Preact berakhir di lazy.js, yang ternyata sedikit rumit karena Preact tidak dapat menangani komponen yang dimuat secara lambat sejak pertama kali digunakan. Karena alasan ini, kita 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 ini, kita dapat menggunakan Promise sebuah 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 yang 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 20 KB saja, kurang dari setengah dari ukuran aslinya. Apa pengaruhnya terhadap FMP dan TTI? WebPageTest akan memberi tahu!

Setrip film mengonfirmasi: TTI kami sekarang pada 5,4 detik. Peningkatan drastis dari 11 level sebelumnya.

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

Lebih Mudah

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 hingga kami memiliki mesin rendering untuk merender game. Kita dapat menonaktifkan tombol "Start" hingga mesin render kita siap memulai game, tetapi berdasarkan pengalaman kami, pengguna biasanya membutuhkan waktu cukup lama untuk mengonfigurasi setelan game sehingga hal ini tidak diperlukan. Biasanya, mesin rendering dan modul lainnya selesai dimuat saat pengguna menekan "Start". Dalam kasus yang jarang terjadi, ketika pengguna lebih cepat daripada koneksi jaringan, kami akan menampilkan layar pemuatan sederhana yang menunggu modul lainnya selesai.

Kesimpulan

Mengukur adalah hal penting. Untuk menghindari menghabiskan waktu pada masalah yang tidak nyata, sebaiknya selalu lakukan pengukuran terlebih dahulu sebelum menerapkan pengoptimalan. Selain itu, pengukuran harus dilakukan pada perangkat sungguhan di koneksi 3G atau di WebPageTest jika tidak ada perangkat sungguhan.

Setrip film dapat memberikan insight tentang perasaan pemuatan aplikasi bagi pengguna. Waterfall dapat memberi tahu Anda resource apa yang bertanggung jawab atas waktu pemuatan yang berpotensi lama. Berikut adalah checklist hal-hal yang dapat Anda lakukan untuk meningkatkan performa pemuatan:

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

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