Menemukan interaksi lambat di lapangan

Pelajari cara menemukan interaksi lambat dalam data kolom situs Anda sehingga Anda dapat menemukan peluang untuk meningkatkan Interaction to Next Paint-nya.

Data kolom adalah data yang memberi tahu Anda pengalaman pengguna yang sebenarnya di situs Anda. Hal ini akan mengungkapkan masalah yang tidak dapat Anda temukan di data lab saja. Untuk Interaction to Next Paint (INP), data kolom sangat penting dalam mengidentifikasi interaksi yang lambat, dan memberikan petunjuk penting untuk membantu Anda memperbaikinya.

Dalam panduan ini, Anda akan mempelajari cara menilai INP situs dengan cepat menggunakan data kolom dari Laporan Pengalaman Pengguna Chrome (CrUX) untuk melihat apakah situs Anda mengalami masalah terkait INP. Selanjutnya, Anda akan mempelajari cara menggunakan build atribusi library JavaScript web-vitals—dan insight baru yang diberikannya dari Long Animation Frames API (LoAF)—untuk mengumpulkan dan menafsirkan data lapangan untuk interaksi lambat di situs Anda.

Mulai dengan CrUX untuk mengevaluasi INP situs Anda

Jika Anda tidak mengumpulkan data lapangan dari pengguna situs, CrUX mungkin merupakan titik awal yang baik. CrUX mengumpulkan data kolom dari pengguna Chrome sebenarnya yang telah memilih untuk mengirim data telemetri.

Data CrUX ditampilkan di sejumlah area yang berbeda, dan bergantung pada cakupan informasi yang Anda cari. CrUX dapat memberikan data tentang INP dan Core Web Vitals lainnya untuk:

  • Setiap halaman dan seluruh origin menggunakan PageSpeed Insights.
  • Jenis halaman. Misalnya, banyak situs e-commerce memiliki jenis Halaman Detail Produk dan Halaman Listingan Produk. Anda bisa mendapatkan data CrUX untuk jenis halaman unik di Search Console.

Sebagai titik awal, Anda dapat memasukkan URL situs di PageSpeed Insights. Setelah Anda memasukkan URL, data kolom untuk URL tersebut—jika tersedia—akan ditampilkan untuk beberapa metrik, termasuk INP. Anda juga dapat menggunakan tombol untuk memeriksa nilai INP untuk dimensi seluler dan desktop.

Data kolom seperti yang ditampilkan oleh CrUX di PageSpeed Insights, yang menampilkan LCP, INP, CLS pada tiga Core Web Vitals, serta TTFB, FCP sebagai metrik diagnostik, dan FID sebagai metrik Core Web Vitals yang tidak digunakan lagi.
Pembacaan data CrUX seperti yang terlihat di PageSpeed Insights. Dalam contoh ini, INP halaman web yang diberikan perlu ditingkatkan kualitasnya.

Data ini berguna karena memberi tahu Anda jika ada masalah. Namun, CrUX tidak dapat memberi tahu Anda apa yang menyebabkan masalah. Ada banyak solusi Real User Monitoring (RUM) yang tersedia untuk membantu Anda mengumpulkan data kolom sendiri dari pengguna situs untuk membantu Anda menjawab pertanyaan tersebut, dan salah satu opsi adalah mengumpulkan data kolom tersebut sendiri menggunakan library JavaScript web-vitals.

Mengumpulkan data kolom dengan library JavaScript web-vitals

Library JavaScript web-vitals adalah skrip yang dapat Anda muat di situs untuk mengumpulkan data kolom dari pengguna situs Anda. Anda dapat menggunakannya untuk mencatat sejumlah metrik, termasuk INP di browser yang mendukungnya.

Dukungan Browser

  • Chrome: 96.
  • Edge: 96.
  • Firefox: tidak didukung.
  • Safari: tidak didukung.

Sumber

Build standar library web-vitals dapat digunakan untuk mendapatkan data INP dasar dari pengguna di lapangan:

import {onINP} from 'web-vitals';

onINP
(({name, value, rating}) => {
  console
.log(name);    // 'INP'
  console
.log(value);   // 512
  console
.log(rating);  // 'poor'
});

Untuk menganalisis data lapangan dari pengguna, Anda harus mengirimkan data ini ke suatu tempat:

import {onINP} from 'web-vitals';

onINP
(({name, value, rating}) => {
 
// Prepare JSON to be sent for collection. Note that
 
// you can add anything else you'd want to collect here:
 
const body = JSON.stringify({name, value, rating});

 
// Use `sendBeacon` to send data to an analytics endpoint.
 
// For Google Analytics, see https://github.com/GoogleChrome/web-vitals#send-the-results-to-google-analytics.
  navigator
.sendBeacon('/analytics', body);
});

Namun, data ini sendiri tidak memberi tahu Anda lebih banyak daripada CrUX. Di sinilah build atribusi library web-vitals berperan.

Mempelajari lebih lanjut build atribusi library web-vitals

Build atribusi library data web menampilkan data tambahan yang dapat Anda dapatkan dari pengguna di lapangan untuk membantu Anda memecahkan masalah interaksi bermasalah yang memengaruhi INP situs Anda dengan lebih baik. Data ini dapat diakses melalui objek attribution yang ditampilkan dalam metode onINP() library:

import {onINP} from 'web-vitals/attribution';

onINP
(({name, value, rating, attribution}) => {
  console
.log(name);         // 'INP'
  console
.log(value);        // 56
  console
.log(rating);       // 'good'
  console
.log(attribution);  // Attribution data object
});
Tampilan log konsol dari library web-vitals. Konsol dalam contoh ini menampilkan nama metrik (INP), nilai INP (56), tempat nilai tersebut berada dalam nilai minimum INP (baik), dan berbagai bit informasi yang ditampilkan dalam objek atribusi, termasuk entri dari Long Animation Frames API.
Cara data dari library web-vitals muncul di konsol.

Selain INP halaman itu sendiri, build atribusi menyediakan banyak data yang dapat Anda gunakan untuk membantu memahami alasan interaksi lambat, termasuk bagian interaksi mana yang harus Anda fokuskan. Pemodelan perilaku dapat membantu Anda menjawab pertanyaan penting seperti:

  • "Apakah pengguna berinteraksi dengan halaman saat halaman dimuat?"
  • "Apakah pengendali peristiwa interaksi berjalan dalam waktu lama?"
  • "Apakah kode pengendali peristiwa interaksi tertunda untuk dimulai? Jika ya, apa lagi yang terjadi di thread utama pada saat itu?"
  • "Apakah interaksi menyebabkan banyak pekerjaan rendering yang menunda proses rendering frame berikutnya?"

Tabel berikut menunjukkan beberapa data atribusi dasar yang dapat Anda dapatkan dari library yang dapat membantu Anda mengetahui beberapa penyebab umum interaksi lambat di situs Anda:

Kunci objek attribution Data
interactionTarget Pemilih CSS yang mengarah ke elemen yang menghasilkan nilai INP halaman—misalnya, button#save.
interactionType Jenis interaksi, baik dari klik, ketukan, maupun input keyboard.
inputDelay* Penundaan input interaksi.
processingDuration* Waktu sejak pemroses peristiwa pertama mulai berjalan sebagai respons terhadap interaksi pengguna hingga semua pemrosesan pemroses peristiwa selesai.
presentationDelay* Penundaan presentasi interaksi, yang terjadi mulai dari saat pengendali peristiwa selesai hingga saat frame berikutnya digambar.
longAnimationFrameEntries* Entri dari LoAF yang terkait dengan interaksi. Lihat bagian berikutnya untuk mengetahui info tambahan.
*Baru di versi 4

Mulai dari library web-vitals versi 4, Anda bisa mendapatkan insight yang lebih mendalam tentang interaksi yang bermasalah melalui data yang disediakan dengan perincian fase INP (input delay, processing duration, dan presentation delay) dan Long Animation Frames API (LoAF).

Long Animation Frames API (LoAF)

Dukungan Browser

  • Chrome: 123.
  • Edge: 123.
  • Firefox: tidak didukung.
  • Safari: tidak didukung.

Sumber

Men-debug interaksi menggunakan data kolom adalah tugas yang menantang. Namun, dengan data dari LoAF, kini Anda dapat memperoleh insight yang lebih baik tentang penyebab interaksi yang lambat, karena LoAF mengekspos sejumlah pengaturan waktu mendetail dan data lainnya yang dapat Anda gunakan untuk menentukan penyebab yang tepat—dan yang lebih penting, lokasi sumber masalah dalam kode situs Anda.

Build atribusi library web-vitals mengekspos array entri LoAF pada kunci longAnimationFrameEntries objek attribution. Tabel berikut mencantumkan beberapa bit data utama yang dapat Anda temukan di setiap entri LoAF:

Kunci objek entri LoAF Data
duration Durasi frame animasi panjang, hingga tata letak selesai, tetapi tidak termasuk proses menggambar dan pengomposisian.
blockingDuration Jumlah total waktu dalam frame saat browser tidak dapat merespons dengan cepat karena tugas yang lama. Waktu pemblokiran ini dapat mencakup tugas panjang yang menjalankan JavaScript, serta tugas rendering panjang berikutnya dalam frame.
firstUIEventTimestamp Stempel waktu saat peristiwa diantrekan selama frame. Berguna untuk mengetahui awal penundaan input interaksi.
startTime Stempel waktu awal frame.
renderStart Saat pekerjaan rendering untuk frame dimulai. Hal ini mencakup callback requestAnimationFrame (dan callback ResizeObserver jika berlaku), tetapi berpotensi sebelum pekerjaan gaya/tata letak dimulai.
styleAndLayoutStart Saat gaya/tata letak berfungsi dalam frame. Dapat berguna dalam mengetahui durasi pekerjaan gaya/tata letak saat menghitung stempel waktu lain yang tersedia.
scripts Array item yang berisi informasi atribusi skrip yang berkontribusi pada INP halaman.
Visualisasi frame animasi panjang sesuai dengan model LoAF.
Diagram pengaturan waktu frame animasi panjang sesuai dengan LoAF API (minus blockingDuration).

Semua informasi ini dapat memberi tahu Anda banyak hal tentang apa yang membuat interaksi menjadi lambat—tetapi array scripts yang ditampilkan entri LoAF harus menjadi perhatian khusus:

Kunci objek atribusi skrip Data
invoker Pemanggil. Hal ini dapat bervariasi berdasarkan jenis pemanggil yang dijelaskan di baris berikutnya. Contoh pemanggil dapat berupa nilai seperti 'IMG#id.onload', 'Window.requestAnimationFrame', atau 'Response.json.then'.
invokerType Jenis pemanggil. Dapat berupa 'user-callback', 'event-listener', 'resolve-promise', 'reject-promise', 'classic-script', atau 'module-script'.
sourceURL URL ke skrip tempat frame animasi panjang berasal.
sourceCharPosition Posisi karakter dalam skrip yang diidentifikasi oleh sourceURL.
sourceFunctionName Nama fungsi dalam skrip yang diidentifikasi.

Setiap entri dalam array ini berisi data yang ditampilkan dalam tabel ini, yang memberi Anda informasi tentang skrip yang bertanggung jawab atas interaksi yang lambat—dan bagaimana skrip tersebut bertanggung jawab.

Mengukur dan mengidentifikasi penyebab umum di balik interaksi yang lambat

Untuk memberi Anda gambaran tentang cara menggunakan informasi ini, panduan ini sekarang akan menjelaskan cara menggunakan data LoAF yang ditampilkan di library web-vitals untuk menentukan beberapa penyebab interaksi lambat.

Durasi pemrosesan yang lama

Durasi pemrosesan interaksi adalah waktu yang diperlukan callback pengendali peristiwa terdaftar interaksi untuk berjalan hingga selesai dan hal lain yang mungkin terjadi di antaranya. Durasi pemrosesan yang tinggi ditampilkan oleh library web-vitals:

import {onINP} from 'web-vitals/attribution';

onINP
(({name, value, attribution}) => {
 
const {processingDuration} = attribution; // 512.5
});

Wajar jika Anda berpikir bahwa penyebab utama interaksi yang lambat adalah kode pengendali peristiwa Anda membutuhkan waktu terlalu lama untuk dijalankan, tetapi tidak selalu demikian. Setelah mengonfirmasi bahwa ini adalah masalahnya, Anda dapat mempelajari lebih lanjut dengan data LoAF:

import {onINP} from 'web-vitals/attribution';

onINP
(({name, value, attribution}) => {
 
const {processingDuration} = attribution; // 512.5

 
// Get the longest script from LoAF covering `processingDuration`:
 
const loaf = attribution.longAnimationFrameEntries.at(-1);
 
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

 
if (script) {
   
// Get attribution for the long-running event handler:
   
const {invokerType} = script;        // 'event-listener'
   
const {invoker} = script;            // 'BUTTON#update.onclick'
   
const {sourceURL} = script;          // 'https://example.com/app.js'
   
const {sourceCharPosition} = script; // 83
   
const {sourceFunctionName} = script; // 'update'
 
}
});

Seperti yang dapat Anda lihat di cuplikan kode sebelumnya, Anda dapat menggunakan data LoAF untuk melacak penyebab yang tepat di balik interaksi dengan nilai durasi pemrosesan yang tinggi, termasuk:

  • Elemen dan pemroses peristiwa terdaftarnya.
  • File skrip—dan posisi karakter di dalamnya—berisi kode pengendali peristiwa yang berjalan lama.
  • Nama fungsi.

Jenis data ini sangat berharga. Anda tidak perlu lagi melakukan pekerjaan berat untuk mencari tahu interaksi mana—atau pengendali peristiwa mana—yang bertanggung jawab atas nilai durasi pemrosesan yang tinggi. Selain itu, karena skrip pihak ketiga sering kali dapat mendaftarkan pengendali peristiwanya sendiri, Anda dapat menentukan apakah kode Anda yang bertanggung jawab atau tidak. Untuk kode yang dapat Anda kontrol, sebaiknya pelajari cara mengoptimalkan tugas yang lama.

Penundaan input yang lama

Meskipun pengendali peristiwa yang berjalan lama sudah umum, ada bagian interaksi lain yang perlu dipertimbangkan. Satu bagian terjadi sebelum durasi pemrosesan, yang dikenal sebagai penundaan input. Ini adalah waktu sejak pengguna memulai interaksi, hingga saat callback pengendali peristiwanya mulai berjalan dan terjadi saat thread utama sudah memproses tugas lain. Build atribusi library web-vitals dapat memberi tahu Anda durasi penundaan input untuk interaksi:

import {onINP} from 'web-vitals/attribution';

onINP
(({name, value, attribution}) => {
 
const {inputDelay} = attribution; // 125.59439536
});

Jika Anda melihat bahwa beberapa interaksi memiliki penundaan input yang tinggi, Anda harus mencari tahu apa yang terjadi di halaman pada saat interaksi yang menyebabkan penundaan input yang lama—dan hal ini sering kali bergantung pada apakah interaksi terjadi saat halaman dimuat, atau setelahnya.

Apakah selama pemuatan halaman?

Thread utama sering kali paling sibuk saat halaman dimuat. Selama waktu ini, semua jenis tugas sedang diantrekan dan diproses, dan jika pengguna mencoba berinteraksi dengan halaman saat semua pekerjaan ini sedang berlangsung, hal ini dapat menunda interaksi. Halaman yang memuat banyak JavaScript dapat memulai pekerjaan untuk mengompilasi dan mengevaluasi skrip, serta menjalankan fungsi yang menyiapkan halaman untuk interaksi pengguna. Pekerjaan ini dapat mengganggu jika pengguna kebetulan berinteraksi saat aktivitas ini terjadi, dan Anda dapat mengetahui apakah hal itu terjadi pada pengguna situs Anda:

import {onINP} from 'web-vitals/attribution';

onINP
(({name, value, attribution}) => {
 
const {inputDelay} = attribution; // 125.59439536

 
// Get the longest script from the first LoAF entry:
 
const loaf = attribution.longAnimationFrameEntries[0];
 
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

 
if (script) {
   
// Invoker types can describe if script eval blocked the main thread:
   
const {invokerType} = script;    // 'classic-script' | 'module-script'
   
const {sourceLocation} = script; // 'https://example.com/app.js'
 
}
});

Jika Anda mencatat data ini di kolom dan melihat penundaan input yang tinggi serta jenis pemanggil 'classic-script' atau 'module-script', dapat dikatakan bahwa skrip di situs Anda memerlukan waktu lama untuk dievaluasi, dan memblokir thread utama cukup lama untuk menunda interaksi. Anda dapat mengurangi waktu pemblokiran ini dengan membagi skrip menjadi beberapa paket yang lebih kecil, menunda kode yang awalnya tidak digunakan untuk dimuat pada waktu yang akan datang, dan mengaudit situs Anda untuk menemukan kode yang tidak digunakan yang dapat Anda hapus sepenuhnya.

Apakah setelah pemuatan halaman?

Meskipun penundaan input sering terjadi saat halaman dimuat, penundaan input juga dapat terjadi setelah halaman dimuat, karena penyebabnya sama sekali berbeda. Penyebab umum penundaan input setelah pemuatan halaman dapat berupa kode yang berjalan secara berkala karena panggilan setInterval sebelumnya, atau bahkan callback peristiwa yang diantrekan untuk dijalankan lebih awal, dan masih diproses.

import {onINP} from 'web-vitals/attribution';

onINP
(({name, value, attribution}) => {
 
const {inputDelay} = attribution; // 125.59439536

 
// Get the longest script from the first LoAF entry:
 
const loaf = attribution.longAnimationFrameEntries[0];
 
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

 
if (script) {
   
const {invokerType} = script;        // 'user-callback'
   
const {sourceURL} = script;          // 'https://example.com/app.js'
   
const {sourceCharPosition} = script; // 83
   
const {sourceFunctionName} = script; // 'update'
 
}
});

Seperti halnya pemecahan masalah nilai durasi pemrosesan yang tinggi, penundaan input yang tinggi karena penyebab yang disebutkan sebelumnya akan memberi Anda data atribusi skrip yang mendetail. Namun, yang berbeda adalah jenis pemanggil akan berubah berdasarkan sifat pekerjaan yang menunda interaksi:

  • 'user-callback' menunjukkan bahwa tugas pemblokiran berasal dari setInterval, setTimeout, atau bahkan requestAnimationFrame.
  • 'event-listener' menunjukkan bahwa tugas pemblokiran berasal dari input sebelumnya yang diantrekan dan masih diproses.
  • 'resolve-promise' dan 'reject-promise' berarti bahwa tugas pemblokiran berasal dari beberapa pekerjaan asinkron yang dimulai sebelumnya, dan diselesaikan atau ditolak pada saat pengguna mencoba berinteraksi dengan halaman, sehingga menunda interaksi.

Apa pun masalahnya, data atribusi skrip akan memberi Anda gambaran tentang tempat untuk mulai mencari, dan apakah penundaan input disebabkan oleh kode Anda sendiri, atau skrip pihak ketiga.

Penundaan presentasi yang lama

Penundaan presentasi adalah tahap terakhir dari interaksi, dan dimulai saat pengendali peristiwa interaksi selesai, hingga titik saat frame berikutnya digambar. Hal ini terjadi saat pekerjaan di pengendali peristiwa karena interaksi mengubah status visual antarmuka pengguna. Seperti durasi pemrosesan dan penundaan input, library web-vitals dapat memberi tahu Anda berapa lama penundaan presentasi untuk suatu interaksi:

import {onINP} from 'web-vitals/attribution';

onINP
(({name, value, attribution}) => {
 
const {presentationDelay} = attribution; // 113.32307691
});

Jika Anda mencatat data ini dan melihat penundaan presentasi yang tinggi untuk interaksi yang berkontribusi pada INP situs Anda, penyebabnya dapat bervariasi, tetapi berikut beberapa penyebab yang perlu diwaspadai.

Gaya dan tata letak yang mahal

Keterlambatan presentasi yang lama mungkin merupakan pekerjaan penghitungan ulang gaya dan tata letak yang mahal yang muncul dari sejumlah penyebab, termasuk pemilih CSS yang kompleks dan ukuran DOM yang besar. Anda dapat mengukur durasi pekerjaan ini dengan pengaturan waktu LoAF yang ditampilkan di library web-vitals:

import {onINP} from 'web-vitals/attribution';

onINP
(({name, value, attribution}) => {
 
const {presentationDelay} = attribution; // 113.32307691

 
// Get the longest script from the last LoAF entry:
 
const loaf = attribution.longAnimationFrameEntries.at(-1);
 
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

 
// Get necessary timings:
 
const {startTime} = loaf; // 2120.5
 
const {duration} = loaf;  // 1002

 
// Figure out the ending timestamp of the frame (approximate):
 
const endTime = startTime + duration; // 3122.5

 
// Get the start timestamp of the frame's style/layout work:
 
const {styleAndLayoutStart} = loaf; // 3011.17692309

 
// Calculate the total style/layout duration:
 
const styleLayoutDuration = endTime - styleAndLayoutStart; // 111.32307691

 
if (script) {
   
// Get attribution for the event handler that triggered
   
// the long-running style and layout operation:
   
const {invokerType} = script;        // 'event-listener'
   
const {invoker} = script;            // 'BUTTON#update.onclick'
   
const {sourceURL} = script;          // 'https://example.com/app.js'
   
const {sourceCharPosition} = script; // 83
   
const {sourceFunctionName} = script; // 'update'
 
}
});

LoAF tidak akan memberi tahu Anda durasi pekerjaan gaya dan tata letak untuk sebuah frame, tetapi akan memberi tahu Anda kapan pekerjaan tersebut dimulai. Dengan stempel waktu awal ini, Anda dapat menggunakan data lain dari LoAF untuk menghitung durasi yang akurat dari pekerjaan tersebut dengan menentukan waktu akhir frame, dan mengurangi stempel waktu awal pekerjaan gaya dan tata letak dari waktu tersebut.

Callback requestAnimationFrame yang berjalan lama

Salah satu potensi penyebab penundaan presentasi yang lama adalah pekerjaan yang berlebihan yang dilakukan dalam callback requestAnimationFrame. Konten callback ini dieksekusi setelah pengendali peristiwa selesai berjalan, tetapi tepat sebelum penghitungan ulang gaya dan pekerjaan tata letak.

Callback ini dapat memerlukan waktu yang cukup lama untuk diselesaikan jika pekerjaan yang dilakukan di dalamnya bersifat kompleks. Jika Anda mencurigai nilai penundaan presentasi yang tinggi disebabkan oleh pekerjaan yang Anda lakukan dengan requestAnimationFrame, Anda dapat menggunakan data LoAF yang ditampilkan oleh library web-vitals untuk mengidentifikasi skenario berikut:

onINP(({name, value, attribution}) => {
 
const {presentationDelay} = attribution; // 543.1999999880791

 
// Get the longest script from the last LoAF entry:
 
const loaf = attribution.longAnimationFrameEntries.at(-1);
 
const script = loaf?.scripts.sort((a, b) => b.duration - a.duration)[0];

 
// Get the render start time and when style and layout began:
 
const {renderStart} = loaf;         // 2489
 
const {styleAndLayoutStart} = loaf; // 2989.5999999940395

 
// Calculate the `requestAnimationFrame` callback's duration:
 
const rafDuration = styleAndLayoutStart - renderStart; // 500.59999999403954

 
if (script) {
   
// Get attribution for the event handler that triggered
   
// the long-running requestAnimationFrame callback:
   
const {invokerType} = script;        // 'user-callback'
   
const {invoker} = script;            // 'FrameRequestCallback'
   
const {sourceURL} = script;          // 'https://example.com/app.js'
   
const {sourceCharPosition} = script; // 83
   
const {sourceFunctionName} = script; // 'update'
 
}
});

Jika Anda melihat bahwa sebagian besar waktu penundaan presentasi dihabiskan dalam callback requestAnimationFrame, pastikan pekerjaan yang Anda lakukan dalam callback ini terbatas pada pekerjaan yang menghasilkan pembaruan aktual pada antarmuka pengguna. Pekerjaan lain yang tidak menyentuh DOM atau memperbarui gaya akan menunda frame berikutnya yang digambar secara tidak perlu, jadi berhati-hatilah.

Kesimpulan

Data lapangan adalah sumber informasi terbaik yang dapat Anda gunakan untuk memahami interaksi mana yang bermasalah bagi pengguna sebenarnya di lapangan. Dengan mengandalkan alat pengumpulan data lapangan seperti library JavaScript web-vitals (atau penyedia RUM), Anda dapat lebih yakin tentang interaksi mana yang paling bermasalah, lalu melanjutkan untuk mereproduksi interaksi yang bermasalah di lab, lalu memperbaikinya.

Banner besar dari Unsplash, oleh Federico Respini.