Bagaimana Kami Meningkatkan Kinerja SmashingMag
Diterbitkan: 2022-03-10Artikel ini didukung dengan baik oleh teman-teman terkasih kami di Media Temple yang memberikan spektrum penuh solusi hosting web untuk desainer, pengembang, dan klien Anda. Terima kasih, teman-teman terkasih!
Setiap cerita kinerja web serupa, bukan? Itu selalu dimulai dengan perombakan situs web yang telah lama ditunggu-tunggu. Suatu hari ketika sebuah proyek, yang sepenuhnya dipoles dan dioptimalkan dengan hati-hati, diluncurkan, mendapat peringkat tinggi dan melonjak di atas skor kinerja di Lighthouse dan WebPageTest. Ada perayaan dan rasa pencapaian sepenuh hati yang ada di udara — tercermin dengan indah dalam retweet dan komentar serta buletin dan utas Slack.
Namun seiring berjalannya waktu, kegembiraan perlahan memudar, dan penyesuaian mendesak, fitur yang sangat dibutuhkan, dan persyaratan bisnis baru merayap masuk. Dan tiba-tiba, sebelum Anda menyadarinya, basis kode menjadi sedikit berlebihan dan terfragmentasi , pihak ketiga skrip harus dimuat sedikit lebih awal, dan konten dinamis baru yang mengkilap menemukan jalannya ke DOM melalui pintu belakang skrip pihak keempat dan tamu tak diundang mereka.
Kami juga pernah ke sana di Smashing. Tidak banyak orang yang mengetahuinya tetapi kami adalah tim yang sangat kecil yang terdiri dari sekitar 12 orang, banyak dari mereka bekerja paruh waktu dan kebanyakan dari mereka biasanya mengenakan banyak topi berbeda pada hari tertentu. Sementara kinerja telah menjadi tujuan kami selama hampir satu dekade sekarang, kami tidak pernah benar-benar memiliki tim kinerja yang berdedikasi.
Setelah desain ulang terbaru pada akhir 2017, adalah Ilya Pukhalski di sisi JavaScript (paruh waktu), Michael Riethmueller di sisi CSS (beberapa jam seminggu), dan Anda benar-benar, bermain permainan pikiran dengan CSS kritis dan mencoba untuk menyulap beberapa terlalu banyak hal.
Saat itu terjadi, kami kehilangan jejak kinerja di tengah kesibukan rutinitas sehari-hari. Kami merancang dan membangun berbagai hal, menyiapkan produk baru, memfaktorkan ulang komponen, dan menerbitkan artikel. Jadi pada akhir 2020, segalanya menjadi sedikit di luar kendali, dengan skor Lighthouse merah kekuningan perlahan muncul di seluruh papan. Kami harus memperbaikinya.
Di situlah kita berada
Beberapa dari Anda mungkin tahu bahwa kami menjalankan JAMStack, dengan semua artikel dan halaman disimpan sebagai file penurunan harga, file Sass dikompilasi ke dalam CSS, JavaScript dipecah menjadi beberapa bagian dengan Webpack dan Hugo membangun halaman statis yang kemudian kami sajikan langsung dari Edge CDN. Kembali pada tahun 2017 kami membangun seluruh situs dengan Preact, tetapi kemudian pindah ke React pada tahun 2019 — dan menggunakannya bersama dengan beberapa API untuk pencarian, komentar, autentikasi, dan checkout.
Seluruh situs dibangun dengan mempertimbangkan peningkatan progresif, artinya Anda, pembaca yang budiman, dapat membaca setiap artikel Smashing secara keseluruhan tanpa perlu mem-boot aplikasi sama sekali. Ini juga tidak terlalu mengejutkan — pada akhirnya, artikel yang diterbitkan tidak banyak berubah selama bertahun-tahun, sementara bagian dinamis seperti otentikasi Keanggotaan dan checkout memerlukan aplikasi untuk dijalankan.
Seluruh build untuk menyebarkan sekitar 2500 artikel secara langsung membutuhkan waktu sekitar 6 menit saat ini. Proses pembuatannya sendiri telah menjadi sangat buruk dari waktu ke waktu juga, dengan injeksi CSS kritis, pemecahan kode Webpack, sisipan dinamis dari panel iklan dan fitur, pembuatan RSS (kembali), dan akhirnya pengujian A/B di edge.
Pada awal tahun 2020, kami telah memulai dengan refactoring besar -besaran dari komponen tata letak CSS. Kami tidak pernah menggunakan CSS-in-JS atau komponen gaya, melainkan sistem modul Sass berbasis komponen yang bagus yang akan dikompilasi ke dalam CSS. Kembali pada tahun 2017, seluruh tata letak dibangun dengan Flexbox dan dibangun kembali dengan CSS Grid dan CSS Custom Properties pada pertengahan 2019. Namun, beberapa halaman memerlukan perlakuan khusus karena tempat iklan baru dan panel produk baru. Jadi saat tata letak berfungsi, itu tidak berfungsi dengan baik, dan cukup sulit untuk dipelihara.
Selain itu, header dengan navigasi utama harus diubah untuk mengakomodasi lebih banyak item yang ingin kami tampilkan secara dinamis. Selain itu, kami ingin memfaktorkan ulang beberapa komponen yang sering digunakan di seluruh situs, dan CSS yang digunakan di sana juga memerlukan beberapa revisi — kotak buletin menjadi penyebab paling menonjol. Kami memulai dengan memfaktorkan ulang beberapa komponen dengan CSS yang mengutamakan utilitas, tetapi kami tidak pernah sampai pada titik bahwa itu digunakan secara konsisten di seluruh situs.
Masalah yang lebih besar adalah bundel JavaScript besar yang — tidak terlalu mengejutkan — memblokir utas utama selama ratusan milidetik. Bundel JavaScript besar mungkin tampak tidak pada tempatnya di majalah yang hanya menerbitkan artikel, tetapi sebenarnya, ada banyak skrip yang terjadi di balik layar.
Kami memiliki berbagai status komponen untuk pelanggan yang diautentikasi dan tidak diautentikasi. Setelah Anda masuk, kami ingin menampilkan semua produk dengan harga akhir, dan saat Anda menambahkan buku ke troli, kami ingin troli tetap dapat diakses dengan mengetuk tombol — tidak peduli halaman mana Anda berada. Iklan harus masuk dengan cepat tanpa menyebabkan perubahan tata letak yang mengganggu , dan hal yang sama berlaku untuk panel produk asli yang menyoroti produk kami. Ditambah pekerja layanan yang menyimpan semua aset statis dan menyajikannya untuk tampilan berulang, bersama dengan versi cache artikel yang telah dikunjungi pembaca.
Jadi semua skrip ini harus terjadi di beberapa titik, dan itu menguras pengalaman membaca meskipun skripnya datang cukup terlambat. Terus terang, kami dengan susah payah mengerjakan situs dan komponen baru tanpa memperhatikan kinerja (dan kami memiliki beberapa hal lain yang perlu diingat untuk tahun 2020). Titik balik datang secara tak terduga. Harry Roberts menjalankan Web Performance Masterclass-nya (luar biasa) sebagai lokakarya online bersama kami, dan di seluruh lokakarya, dia menggunakan Smashing sebagai contoh dengan menyoroti masalah yang kami miliki dan menyarankan solusi untuk masalah tersebut di samping alat dan pedoman yang berguna.
Sepanjang lokakarya, saya rajin mencatat dan meninjau kembali basis kode. Pada saat lokakarya, skor Lighthouse kami adalah 60–68 di beranda , dan sekitar 40-60 di halaman artikel — dan jelas lebih buruk di seluler. Setelah lokakarya selesai, kami mulai bekerja.
Mengidentifikasi Kemacetan
Kita sering cenderung mengandalkan skor tertentu untuk mendapatkan pemahaman tentang seberapa baik kinerja kita, namun terlalu sering skor tunggal tidak memberikan gambaran yang lengkap. Seperti yang dicatat dengan fasih oleh David East dalam artikelnya, kinerja web bukanlah nilai tunggal; itu adalah distribusi. Bahkan jika pengalaman web sangat dan secara menyeluruh merupakan kinerja menyeluruh yang dioptimalkan, itu tidak bisa hanya cepat. Mungkin cepat bagi sebagian pengunjung, tetapi pada akhirnya juga akan lebih lambat (atau lambat) bagi sebagian pengunjung lainnya.
Alasannya sangat banyak, tetapi yang paling penting adalah perbedaan besar dalam kondisi jaringan dan perangkat keras perangkat di seluruh dunia. Lebih sering daripada tidak, kita tidak dapat benar-benar mempengaruhi hal-hal itu, jadi kita harus memastikan bahwa pengalaman kita mengakomodasi mereka sebagai gantinya.
Intinya, tugas kita kemudian adalah meningkatkan proporsi pengalaman yang cepat dan mengurangi proporsi pengalaman yang lamban. Namun untuk itu, kita perlu mendapatkan gambaran yang tepat tentang apa sebenarnya distribusi itu. Sekarang, alat analitik dan alat pemantauan kinerja akan menyediakan data ini saat dibutuhkan, tetapi kami melihat secara khusus ke CrUX, Laporan Pengalaman Pengguna Chrome. CrUX menghasilkan ikhtisar distribusi kinerja dari waktu ke waktu, dengan lalu lintas yang dikumpulkan dari pengguna Chrome. Sebagian besar data ini terkait dengan Core Web Vitals yang telah diumumkan Google pada tahun 2020, dan yang juga berkontribusi dan diekspos di Lighthouse.
Kami melihat bahwa secara keseluruhan, kinerja kami menurun secara dramatis sepanjang tahun, dengan penurunan tertentu sekitar bulan Agustus dan September. Setelah kami melihat grafik ini, kami dapat melihat kembali beberapa PR yang telah kami tayangkan saat itu untuk mempelajari apa yang sebenarnya terjadi.
Tidak butuh waktu lama untuk mengetahui bahwa pada saat-saat ini kami meluncurkan bilah navigasi baru secara langsung. Bilah navigasi itu — yang digunakan di semua halaman — mengandalkan JavaScript untuk menampilkan item navigasi dalam menu dengan ketukan atau klik, tetapi bit JavaScript-nya sebenarnya dibundel dalam bundel app.js. Untuk meningkatkan Time To Interactive, kami memutuskan untuk mengekstrak skrip navigasi dari bundel dan menyajikannya secara inline.
Pada waktu yang hampir bersamaan, kami beralih dari file CSS penting yang (ketinggalan zaman) yang dibuat secara manual ke sistem otomatis yang menghasilkan CSS penting untuk setiap template — beranda, artikel, halaman produk, acara, papan pekerjaan, dan sebagainya — dan CSS penting sebaris selama waktu membangun. Namun kami tidak benar-benar menyadari betapa beratnya CSS kritis yang dihasilkan secara otomatis. Kami harus menjelajahinya lebih detail.
Dan juga pada waktu yang hampir bersamaan, kami menyesuaikan pemuatan font web , mencoba mendorong font web lebih agresif dengan petunjuk sumber daya seperti pramuat. Hal ini tampaknya bertentangan dengan upaya kinerja kami, karena font web menunda rendering konten, diprioritaskan di samping file CSS lengkap.
Sekarang, salah satu alasan umum untuk regresi adalah biaya JavaScript yang mahal, jadi kami juga melihat ke Webpack Bundle Analyzer dan peta permintaan Simon Hearne untuk mendapatkan gambaran visual tentang dependensi JavaScript kami. Itu terlihat cukup sehat di awal.
Beberapa permintaan datang ke CDN, layanan izin cookie Cookiebot, Google Analytics, ditambah layanan internal kami untuk menayangkan panel produk dan iklan khusus. Tampaknya tidak ada banyak hambatan — sampai kami melihat lebih dekat.
Dalam pekerjaan kinerja, adalah umum untuk melihat kinerja beberapa halaman penting — kemungkinan besar beranda dan kemungkinan besar beberapa halaman artikel/produk. Namun, meskipun hanya ada satu halaman beranda, mungkin ada banyak halaman produk yang berbeda, jadi kami harus memilih halaman yang mewakili audiens kami.
Faktanya, karena kami menerbitkan beberapa artikel berat kode dan berat desain di SmashingMag, selama bertahun-tahun kami telah mengumpulkan ribuan artikel yang berisi GIF berat, cuplikan kode yang disorot sintaks, penyematan CodePen, video/audio embed, dan utas bersarang dari komentar yang tidak pernah berakhir.
Ketika disatukan, banyak dari mereka yang menyebabkan ledakan ukuran DOM bersama dengan pekerjaan utas utama yang berlebihan — memperlambat pengalaman pada ribuan halaman. Belum lagi dengan pemasangan iklan, beberapa elemen DOM disuntikkan di akhir siklus hidup halaman yang menyebabkan serangkaian penghitungan ulang gaya dan pengecatan ulang — juga tugas mahal yang dapat menghasilkan tugas yang lama.
Semua ini tidak muncul di peta yang kami buat untuk halaman artikel yang cukup ringan di bagan di atas. Jadi kami memilih halaman terberat yang kami miliki — beranda yang maha kuasa, yang terpanjang, yang memiliki banyak penyematan video, dan satu dengan banyak penyematan CodePen — dan memutuskan untuk mengoptimalkannya sebanyak yang kami bisa. Lagi pula, jika cepat, maka halaman dengan satu penyematan CodePen juga harus lebih cepat.
Dengan mengingat halaman-halaman ini, peta terlihat sedikit berbeda. Perhatikan garis tebal besar menuju pemutar Vimeo dan CDN Vimeo, dengan 78 permintaan yang berasal dari artikel Smashing.
Untuk mempelajari dampak pada utas utama, kami mendalami panel Performa di DevTools. Lebih khusus lagi, kami mencari tugas yang berlangsung lebih dari 50 md (disorot dengan persegi panjang merah di sudut kanan atas) dan tugas yang berisi gaya Perhitungan Ulang (bilah ungu). Yang pertama akan menunjukkan eksekusi JavaScript yang mahal, sedangkan yang kedua akan mengekspos pembatalan gaya yang disebabkan oleh injeksi dinamis konten di DOM dan CSS yang kurang optimal. Ini memberi kami beberapa petunjuk yang dapat ditindaklanjuti untuk memulai. Misalnya, kami dengan cepat menemukan bahwa pemuatan font web kami memiliki biaya pengecatan ulang yang signifikan, sementara potongan JavaScript masih cukup berat untuk memblokir utas utama.
Sebagai dasar, kami mengamati dengan cermat Core Web Vitals, mencoba memastikan bahwa kami mendapatkan skor yang baik di semua data tersebut. Kami memilih untuk fokus secara khusus pada perangkat seluler yang lambat — dengan kecepatan transfer 3G, 400ms RTT, dan 400kbps yang lambat, hanya untuk bersikap pesimistis. Tidak mengherankan jika Lighthouse juga tidak terlalu senang dengan situs kami, memberikan skor merah yang solid untuk artikel terberat, dan tanpa lelah mengeluh tentang JavaScript, CSS, gambar di luar layar, dan ukurannya yang tidak digunakan.
Setelah kami memiliki beberapa data di depan kami, kami dapat fokus untuk mengoptimalkan tiga halaman artikel terberat, dengan fokus pada CSS penting (dan tidak kritis), bundel JavaScript, tugas panjang, pemuatan font web, perubahan tata letak, dan pihak ketiga -menyematkan. Nanti kami juga akan merevisi basis kode untuk menghapus kode lama dan menggunakan fitur browser modern baru. Sepertinya banyak pekerjaan di depan, dan memang kami cukup sibuk untuk bulan-bulan mendatang.
Memperbaiki Urutan Aset Di <head>
Ironisnya, hal pertama yang kami periksa bahkan tidak terkait erat dengan semua tugas yang telah kami identifikasi di atas. Dalam lokakarya kinerja, Harry menghabiskan banyak waktu untuk menjelaskan urutan aset di <head>
setiap halaman, dengan menegaskan bahwa mengirimkan konten penting dengan cepat berarti menjadi sangat strategis dan memperhatikan bagaimana aset dipesan dalam kode sumber. .
Sekarang seharusnya tidak muncul sebagai wahyu besar bahwa CSS penting bermanfaat untuk kinerja web. Namun, itu memang mengejutkan betapa banyak perbedaan urutan semua aset lainnya — petunjuk sumber daya, pramuat font web, skrip sinkron dan asinkron, CSS dan metadata lengkap — miliki.
Kami telah membalikkan seluruh <head>
, menempatkan CSS penting sebelum semua skrip asinkron dan semua aset yang dimuat sebelumnya seperti font, gambar, dll. Kami telah mengelompokkan aset yang akan kami sambungkan atau pramuat dengan template dan jenis file, sehingga gambar kritis, penyorotan sintaks, dan penyematan video akan diminta lebih awal hanya untuk jenis artikel dan halaman tertentu.
Secara umum, kami telah mengatur urutan dengan hati-hati di <head>
, mengurangi jumlah aset yang dimuat sebelumnya yang bersaing untuk bandwidth, dan berfokus untuk mendapatkan CSS penting dengan benar. Jika Anda ingin mempelajari lebih dalam beberapa pertimbangan kritis dengan urutan <head>
, Harry menyorotinya dalam artikel tentang CSS dan Kinerja Jaringan. Perubahan ini saja membawa kami sekitar 3-4 poin skor Lighthouse di seluruh papan.
Pindah Dari CSS Kritis Otomatis Kembali Ke CSS Kritis Manual
Memindahkan tag <head>
adalah bagian sederhana dari cerita. Yang lebih sulit adalah pembuatan dan pengelolaan file CSS penting. Kembali pada tahun 2017, kami membuat CSS penting secara manual untuk setiap template, dengan mengumpulkan semua gaya yang diperlukan untuk merender 1000 piksel pertama dengan tinggi di semua lebar layar. Ini tentu saja merupakan tugas yang rumit dan sedikit membosankan, belum lagi masalah pemeliharaan untuk menjinakkan seluruh keluarga file CSS penting dan file CSS lengkap.
Jadi kami melihat opsi untuk mengotomatiskan proses ini sebagai bagian dari rutinitas build. Tidak ada kekurangan alat yang tersedia, jadi kami telah menguji beberapa dan memutuskan untuk menjalankan beberapa tes. Kami telah berhasil mengaturnya dan menjalankannya dengan cukup cepat. Outputnya tampaknya cukup baik untuk proses otomatis, jadi setelah beberapa penyesuaian konfigurasi, kami memasangnya dan mendorongnya ke produksi. Itu terjadi sekitar Juli–Agustus tahun lalu, yang divisualisasikan dengan baik dalam lonjakan dan penurunan kinerja pada data CrUX di atas. Kami terus bolak-balik dengan konfigurasi, sering mengalami masalah dengan hal-hal sederhana seperti menambahkan gaya tertentu atau menghapus yang lain. Misalnya, gaya prompt persetujuan cookie yang tidak benar-benar disertakan pada halaman kecuali skrip cookie telah diinisialisasi.
Pada bulan Oktober, kami telah memperkenalkan beberapa perubahan tata letak utama ke situs, dan ketika melihat ke CSS kritis, kami telah mengalami masalah yang sama persis lagi — hasil yang dihasilkan cukup bertele-tele, dan tidak sesuai dengan yang kami inginkan . Jadi sebagai eksperimen di akhir Oktober, kami semua menggabungkan kekuatan kami untuk meninjau kembali pendekatan CSS kritis kami dan mempelajari seberapa kecil CSS penting yang dibuat dengan tangan. Kami mengambil napas dalam-dalam dan menghabiskan waktu berhari-hari di sekitar alat cakupan kode di halaman-halaman utama. Kami mengelompokkan aturan CSS secara manual dan menghapus duplikat dan kode lama di kedua tempat — CSS penting dan CSS utama. Itu memang pembersihan yang sangat dibutuhkan, karena banyak gaya yang ditulis pada 2017–2018 telah menjadi usang selama bertahun-tahun.
Hasilnya, kami mendapatkan tiga file CSS penting yang dibuat dengan tangan, dan dengan tiga file lagi yang sedang dalam proses:
- critical-homepage-manual.css (8.2 KB, Brotlified)
- critical-article-manual.css (8 KB, Brotlified)
- critical-articles-manual.css (6 KB, Brotlified)
- critical-books-manual.css ( pekerjaan yang harus dilakukan )
- critical-events-manual.css ( pekerjaan yang harus dilakukan )
- critical-job-board-manual.css ( pekerjaan yang harus dilakukan )
File-file tersebut digariskan di kepala setiap template, dan saat ini mereka diduplikasi dalam bundel CSS monolitik yang berisi semua yang pernah digunakan (atau tidak benar-benar digunakan lagi) di situs. Saat ini, kami sedang mempertimbangkan untuk memecah bundel CSS lengkap menjadi beberapa paket CSS, sehingga pembaca majalah tidak akan mengunduh gaya dari papan pekerjaan atau halaman buku, tetapi kemudian ketika mencapai halaman tersebut akan mendapatkan render cepat dengan CSS kritis dan dapatkan sisa CSS untuk halaman itu secara asinkron — hanya di halaman itu.
Memang, ukuran file CSS penting yang dibuat dengan tangan tidak jauh lebih kecil: kami telah mengurangi ukuran file CSS penting sekitar 14% . Namun, mereka menyertakan semua yang kami butuhkan dalam urutan yang benar dari atas hingga selesai tanpa duplikat dan gaya yang mengesampingkan. Ini tampaknya menjadi langkah ke arah yang benar, dan itu memberi kami dorongan Lighthouse dari 3-4 poin lainnya. Kami membuat kemajuan.
Mengubah Pemuatan Font Web
Dengan font-display
di ujung jari kita, pemuatan font tampaknya menjadi masalah di masa lalu. Sayangnya, itu tidak tepat dalam kasus kami. Anda, para pembaca yang budiman, sepertinya mengunjungi sejumlah artikel di Majalah Smashing. Anda juga sering kembali ke situs untuk membaca artikel lain — mungkin beberapa jam atau hari kemudian, atau mungkin seminggu kemudian. Salah satu masalah yang kami miliki dengan font-display
digunakan di seluruh situs adalah bahwa untuk pembaca yang sering berpindah antar artikel, kami melihat banyak kilatan antara font fallback dan font web (yang biasanya tidak terjadi karena font akan di-cache dengan benar).
Itu tidak terasa seperti pengalaman pengguna yang layak, jadi kami mencari opsi. Pada Smashing, kami menggunakan dua tipografi utama — Mija untuk heading dan Elena untuk body copy. Mija datang dalam dua bobot (Regular dan Tebal), sementara Elena datang dalam tiga bobot (Regular, Italic, Bold). Kami menjatuhkan Elena's Bold Italic bertahun-tahun yang lalu selama desain ulang hanya karena kami menggunakannya hanya pada beberapa halaman. Kami mensubset font lain dengan menghapus karakter yang tidak digunakan dan rentang Unicode.
Artikel kami sebagian besar diatur dalam teks, jadi kami telah menemukan bahwa sebagian besar waktu di situs, Cat Konten Terbesar adalah paragraf pertama teks dalam artikel atau foto penulis. Itu berarti bahwa kita perlu lebih berhati-hati untuk memastikan bahwa paragraf pertama muncul dengan cepat di font fallback, sementara dengan anggun mengubah ke font web dengan reflow minimal.
Perhatikan baik-baik pengalaman pemuatan awal halaman depan (diperlambat tiga kali):
Kami memiliki empat tujuan utama saat mencari solusi:
- Pada kunjungan pertama, segera render teks dengan font fallback;
- Mencocokkan metrik font font fallback dan font web untuk meminimalkan pergeseran tata letak;
- Muat semua font web secara asinkron dan terapkan semuanya sekaligus (maks. 1 reflow);
- Pada kunjungan berikutnya, render semua teks secara langsung dalam font web (tanpa flashing atau reflow).
Awalnya, kami benar-benar mencoba menggunakan font-display: swap on font-face
. Ini tampaknya menjadi pilihan paling sederhana, namun, seperti yang disebutkan di atas, beberapa pembaca akan mengunjungi sejumlah halaman, jadi kami berakhir dengan banyak kedipan dengan enam font yang kami render di seluruh situs. Selain itu, dengan tampilan font saja, kami tidak dapat mengelompokkan permintaan atau pengecatan ulang.
Ide lain adalah untuk merender semuanya dalam font fallback pada kunjungan awal , lalu meminta dan menyimpan semua font secara asinkron, dan hanya pada kunjungan berikutnya mengirimkan font web langsung dari cache. Masalah dengan pendekatan ini adalah bahwa sejumlah pembaca berasal dari mesin pencari, dan setidaknya beberapa dari mereka hanya akan melihat satu halaman itu — dan kami tidak ingin merender artikel dalam font sistem saja.
Jadi apa itu?
Sejak 2017, kami telah menggunakan pendekatan Two-Stage-Render untuk pemuatan font web yang pada dasarnya menjelaskan dua tahap rendering: satu dengan subset minimal font web, dan yang lainnya dengan rangkaian bobot font yang lengkap. Dulu, kami membuat subset minimal Mija Bold dan Elena Regular yang merupakan bobot paling sering digunakan di situs. Kedua himpunan bagian hanya menyertakan karakter Latin, tanda baca, angka, dan beberapa karakter khusus. Font ini ( ElenaInitial.woff2 dan MijaInitial.woff2 ) berukuran sangat kecil — seringkali hanya berukuran sekitar 10-15 KB. Kami melayani mereka di tahap pertama rendering font, menampilkan seluruh halaman dalam dua font ini.
Kami melakukannya dengan Font Loading API yang memberi kami informasi tentang font mana yang berhasil dimuat dan mana yang belum. Di balik layar, itu terjadi dengan menambahkan kelas .wf-loaded-stage1 ke body , dengan gaya yang merender konten dalam font tersebut:
.wf-loaded-stage1 article, .wf-loaded-stage1 promo-box, .wf-loaded-stage1 comments { font-family: ElenaInitial,sans-serif; } .wf-loaded-stage1 h1, .wf-loaded-stage1 h2, .wf-loaded-stage1 .btn { font-family: MijaInitial,sans-serif; }
Karena file font cukup kecil, mudah-mudahan mereka bisa melalui jaringan dengan cukup cepat. Kemudian karena pembaca benar-benar dapat mulai membaca artikel, kami memuat semua font secara asinkron, dan menambahkan .wf-loaded-stage2 ke badan :
.wf-loaded-stage2 article, .wf-loaded-stage2 promo-box, .wf-loaded-stage2 comments { font-family: Elena,sans-serif; } .wf-loaded-stage2 h1, .wf-loaded-stage2 h2, .wf-loaded-stage2 .btn { font-family: Mija,sans-serif; }
Jadi saat memuat halaman, pembaca akan mendapatkan font web subset kecil dengan cepat terlebih dahulu, dan kemudian kami beralih ke keluarga font lengkap. Sekarang, secara default, peralihan antara font fallback dan font web ini terjadi secara acak, berdasarkan apa pun yang lebih dulu melalui jaringan. Itu mungkin terasa cukup mengganggu saat Anda mulai membaca artikel. Jadi, alih-alih menyerahkannya ke browser untuk memutuskan kapan harus mengganti font, kami mengelompokkan repaints , mengurangi dampak reflow seminimal mungkin.
/* Loading web fonts with Font Loading API to avoid multiple repaints. With help by Irina Lipovaya. */ /* Credit to initial work by Zach Leatherman: https://noti.st/zachleat/KNaZEg/the-five-whys-of-web-font-loading-performance#sWkN4u4 */ // If the Font Loading API is supported... // (If not, we stick to fallback fonts) if ("fonts" in document) { // Create new FontFace objects, one for each font let ElenaRegular = new FontFace( "Elena", "url(/fonts/ElenaWebRegular/ElenaWebRegular.woff2) format('woff2')" ); let ElenaBold = new FontFace( "Elena", "url(/fonts/ElenaWebBold/ElenaWebBold.woff2) format('woff2')", { weight: "700" } ); let ElenaItalic = new FontFace( "Elena", "url(/fonts/ElenaWebRegularItalic/ElenaWebRegularItalic.woff2) format('woff2')", { style: "italic" } ); let MijaBold = new FontFace( "Mija", "url(/fonts/MijaBold/Mija_Bold-webfont.woff2) format('woff2')", { weight: "700" } ); // Load all the fonts but render them at once // if they have successfully loaded let loadedFonts = Promise.all([ ElenaRegular.load(), ElenaBold.load(), ElenaItalic.load(), MijaBold.load() ]).then(result => { result.forEach(font => document.fonts.add(font)); document.documentElement.classList.add('wf-loaded-stage2'); // Used for repeat views sessionStorage.foutFontsStage2Loaded = true; }).catch(error => { throw new Error(`Error caught: ${error}`); }); }
Namun, bagaimana jika subset kecil font pertama tidak masuk melalui jaringan dengan cepat? Kami telah memperhatikan bahwa ini tampaknya terjadi lebih sering daripada yang kami inginkan. Dalam hal ini, setelah batas waktu 3 detik berakhir, browser modern kembali ke font sistem (dalam tumpukan font kami akan menjadi Arial), lalu beralih ke ElenaInitial atau MijaInitial , hanya untuk dialihkan ke Elena atau Mija penuh masing-masing nanti . Itu menghasilkan sedikit terlalu banyak kilatan saat kami mencicipi. Kami awalnya berpikir untuk menghapus render tahap pertama hanya untuk jaringan yang lambat (melalui API Informasi Jaringan), tetapi kemudian kami memutuskan untuk menghapusnya sama sekali.
Jadi pada bulan Oktober, kami menghapus himpunan bagian seluruhnya, bersama dengan tahap perantara. Setiap kali semua bobot font Elena dan Mija berhasil diunduh oleh klien dan siap untuk diterapkan, kami memulai tahap 2 dan mengecat ulang semuanya sekaligus. Dan untuk membuat reflow tidak terlalu terlihat, kami menghabiskan sedikit waktu untuk mencocokkan font fallback dan font web . Itu sebagian besar berarti menerapkan ukuran font dan ketinggian garis yang sedikit berbeda untuk elemen yang dilukis di bagian halaman pertama yang terlihat.
Untuk itu, kami menggunakan font-style-matcher
dan (ahem, ahem) beberapa angka ajaib. Itu juga alasan mengapa kami awalnya menggunakan -apple-system dan Arial sebagai font fallback global; San Francisco (dirender melalui -apple-system ) tampaknya sedikit lebih baik daripada Arial, tetapi jika tidak tersedia, kami memilih untuk menggunakan Arial hanya karena itu tersebar luas di sebagian besar OS.
Di CSS, itu akan terlihat seperti ini:
.article__summary { font-family: -apple-system,Arial,BlinkMacSystemFont,Roboto Slab,Droid Serif,Segoe UI,Ubuntu,Cantarell,Georgia,sans-serif; font-style: italic; /* Warning: magic numbers ahead! */ /* San Francisco Italic and Arial Italic have larger x-height, compared to Elena */ font-size: 0.9213em; line-height: 1.487em; } .wf-loaded-stage2 .article__summary { font-family: Elena,sans-serif; font-size: 1em; /* Original font-size for Elena Italic */ line-height: 1.55em; /* Original line-height for Elena Italic */ }
Ini bekerja dengan cukup baik. Kami langsung menampilkan teks, dan font web masuk di layar dikelompokkan, idealnya menyebabkan tepat satu reflow pada tampilan pertama, dan tidak ada reflow sama sekali pada tampilan berikutnya.
Setelah font diunduh, kami menyimpannya di cache pekerja layanan. Pada kunjungan berikutnya, pertama-tama kami memeriksa apakah font sudah ada di cache. Jika ya, kami mengambilnya dari cache service worker dan segera menerapkannya. Dan jika tidak, kita mulai dari awal dengan fallback-web-font-switcheroo .
Solusi ini mengurangi jumlah reflow ke minimum (satu) pada koneksi yang relatif cepat, sementara juga menjaga font tetap dan andal dalam cache. Di masa depan, kami sangat berharap untuk mengganti angka ajaib dengan f-mods. Mungkin Zach Leatherman akan bangga.
Mengidentifikasi Dan Menghancurkan JS Monolitik
Saat mempelajari utas utama di panel Performa DevTools, kami tahu persis apa yang perlu kami lakukan. Ada delapan Tugas Panjang yang memakan waktu antara 70 md dan 580 md, memblokir antarmuka dan membuatnya tidak responsif. Secara umum, ini adalah skrip yang paling mahal:
- uc.js , skrip prompt cookie (70ms)
- perhitungan ulang gaya yang disebabkan oleh file full.css yang masuk (176 md) (CSS kritis tidak berisi gaya di bawah ketinggian 1000 piksel di semua area pandang)
- skrip iklan berjalan saat memuat acara untuk mengelola panel, keranjang belanja, dll. + penghitungan ulang gaya (276 md)
- sakelar font web, penghitungan ulang gaya (290 md)
- evaluasi app.js (580ms)
Kami fokus pada yang paling berbahaya terlebih dahulu — bisa dibilang Tugas Panjang terpanjang.
Yang pertama terjadi karena perhitungan ulang tata letak yang mahal yang disebabkan oleh perubahan font (dari font fallback ke font web), menyebabkan lebih dari 290 md pekerjaan ekstra (pada laptop cepat dan koneksi cepat). Dengan menghapus tahap satu dari pemuatan font saja, kami dapat memperoleh kembali sekitar 80 md. Itu tidak cukup baik karena jauh di luar anggaran 50 ms. Jadi kami mulai menggali lebih dalam.
Alasan utama mengapa penghitungan ulang terjadi hanyalah karena perbedaan besar antara font fallback dan font web. Dengan mencocokkan tinggi garis dan ukuran untuk font fallback dan font web , kami dapat menghindari banyak situasi ketika baris teks akan membungkus baris baru di font fallback, tetapi kemudian menjadi sedikit lebih kecil dan muat di baris sebelumnya, menyebabkan perubahan besar dalam geometri seluruh halaman, dan akibatnya tata letak bergeser secara besar-besaran. Kami telah bermain dengan letter-spacing
word-spacing
juga, tetapi tidak menghasilkan hasil yang baik.
Dengan perubahan ini, kami dapat memotong 50-80 md lainnya, tetapi kami tidak dapat menguranginya di bawah 120 mdtk tanpa menampilkan konten dalam font cadangan dan menampilkan konten dalam font web setelahnya. Jelas, ini akan mempengaruhi secara besar-besaran hanya pengunjung pertama kali karena tampilan halaman konsekuen akan dirender dengan font yang diambil langsung dari cache service worker, tanpa reflow yang mahal karena pergantian font.
Omong-omong, cukup penting untuk diperhatikan bahwa dalam kasus kami, kami memperhatikan bahwa sebagian besar Tugas Panjang tidak disebabkan oleh JavaScript yang masif, melainkan oleh Perhitungan Ulang Tata Letak dan penguraian CSS, yang berarti bahwa kami perlu melakukan sedikit CSS cleaning, especially watching out for situations when styles are overwritten. In some way, it was good news because we didn't have to deal with complex JavaScript issues that much. However, it turned out not to be straightforward as we are still cleaning up the CSS this very day. We were able to remove two Long Tasks for good, but we still have a few outstanding ones and quite a way to go. Fortunately, most of the time we aren't way above the magical 50ms threshold.
The much bigger issue was the JavaScript bundle we were serving, occupying the main thread for a whopping 580ms. Most of this time was spent in booting up app.js which contains React, Redux, Lodash, and a Webpack module loader. The only way to improve performance with this massive beast was to break it down into smaller pieces. So we looked into doing just that.
With Webpack, we've split up the monolithic bundle into smaller chunks with code-splitting , about 30Kb per chunk. We did some package.json cleansing and version upgrade for all production dependencies, adjusted the browserlistrc setup to address the two latest browser versions, upgraded to Webpack and Babel to the latest versions, moved to Terser for minification, and used ES2017 (+ browserlistrc) as a target for script compilation.
We also used BabelEsmPlugin to generate modern versions of existing dependencies. Finally, we've added prefetch links to the header for all necessary script chunks and refactored the service worker, migrating to Workbox with Webpack (workbox-webpack-plugin).
Remember when we switched to the new navigation back in mid-2020, just to see a huge performance penalty as a result? The reason for it was quite simple. While in the past the navigation was just static plain HTML and a bit of CSS, with the new navigation, we needed a bit of JavaScript to act on opening and closing of the menu on mobile and on desktop. That was causing rage clicks when you would click on the navigation menu and nothing would happen, and of course, had a penalty cost in Time-To-Interactive scores in Lighthouse.
We removed the script from the bundle and extracted it as a separate script . Additionally, we did the same thing for other standalone scripts that were used rarely — for syntax highlighting, tables, video embeds and code embeds — and removed them from the main bundle; instead, we granularly load them only when needed.
However, what we didn't notice for months was that although we removed the navigation script from the bundle, it was loading after the entire app.js bundle was evaluated, which wasn't really helping Time-To-Interactive (see image above). We fixed it by preloading nav.js and deferring it to execute in the order of appearance in the DOM, and managed to save another 100ms with that operation alone. By the end, with everything in place we were able to bring the task to around 220ms.
We managed to get some improvement in place, but still have quite a way to go, with further React and Webpack optimizations on our to-do list. At the moment we still have three major Long Tasks — font switch (120ms), app.js execution (220ms) and style recalculations due to the size of full CSS (140ms). For us, it means cleaning up and breaking up the monolithic CSS next.
It's worth mentioning that these results are really the best-scenario- results. On a given article page we might have a large number of code embeds and video embeds, along with other third-party scripts and customer's browser extensions that would require a separate conversation.
Dealing With 3rd-Parties
Fortunately, our third-party scripts footprint (and the impact of their friends' fourth-party-scripts) wasn't huge from the start. But when these third-party scripts accumulated, they would drive performance down significantly. This goes especially for video embedding scripts , but also syntax highlighting, advertising scripts, promo panels scripts and any external iframe embeds.
Obviously, we defer all of these scripts to start loading after the DOMContentLoaded event, but once they finally come on stage, they cause quite a bit of work on the main thread. This shows up especially on article pages, which are obviously the vast majority of content on the site.
The first thing we did was allocating proper space to all assets that are being injected into the DOM after the initial page render. It meant width
and height
for all advertising images and the styling of code snippets. We found out that because all the scripts were deferred, new styles were invalidating existing styles, causing massive layout shifts for every code snippet that was displayed. We fixed that by adding the necessary styles to the critical CSS on the article pages.
We've re-established a strategy for optimizing images (preferably AVIF or WebP — still work in progress though). All images below the 1000px height threshold are natively lazy-loaded (with <img loading=lazy>
), while the ones on the top are prioritized ( <img loading=eager>
). The same goes for all third-party embeds.
We replaced some dynamic parts with their static counterparts — eg while a note about an article saved for offline reading was appearing dynamically after the article was added to the service worker's cache, now it appears statically as we are, well, a bit optimistic and expect it to be happening in all modern browsers.
As of the moment of writing, we're preparing facades for code embeds and video embeds as well. Plus, all images that are offscreen will get decoding=async
attribute, so the browser has a free reign over when and how it loads images offscreen, asynchronously and in parallel.
To ensure that our images always include width and height attributes, we've also modified Harry Roberts' snippet and Tim Kadlec's diagnostics CSS to highlight whenever an image isn't served properly. It's used in development and editing but obviously not in production.
One technique that we used frequently to track what exactly is happening as the page is being loaded, was slow-motion loading .
First, we've added a simple line of code to the diagnostics CSS, which provides a noticeable outline for all elements on the page.
* { outline: 3px solid red }
* { outline: 3px solid red }
Then we record a video of the page loaded on a slow and fast connection. Then we rewatch the video by slowing down the playback and moving back and forward to identify where massive layout shifts happen.
Here's the recording of a page being loaded on a fast connection:
And here's the recording of a recording being played to study what happens with the layout:
By auditing the layout shifts this way, we were able to quickly notice what's not quite right on the page, and where massive recalculation costs are happening. As you probably have noticed, adjusting the line-height
and font-size
on headings might go a long way to avoid large shifts.
With these simple changes alone, we were able to boost performance score by a whopping 25 Lighthouse points for the video-heaviest article, and gain a few points for code embeds.
Enhancing The Experience
We've tried to be quite strategic in pretty much everything from loading web fonts to serving critical CSS. However, we've done our best to use some of the new technologies that have become available last year.
We are planning on using AVIF by default to serve images on SmashingMag, but we aren't quite there yet, as many of our images are served from Cloudinary (which already has beta support for AVIF), but many are directly from our CDN yet we don't really have a logic in place just yet to generate AVIFs on the fly. That would need to be a manual process for now.
We're lazy rendering some of the offset components of the page with content-visibility: auto . For example, the footer, the comments section, as well as the panels way below the first 1000px height threshold, are all rendered later after the visible portion of each page has been rendered.
Kami telah memainkan sedikit dengan link rel="prefetch"
dan bahkan link rel="prerender"
(NoPush prefetch) beberapa bagian halaman yang kemungkinan besar akan digunakan untuk navigasi lebih lanjut — misalnya, untuk mengambil aset untuk yang pertama artikel di halaman depan (masih dalam diskusi).
Kami juga memuat gambar penulis sebelumnya untuk mengurangi Cat Konten Terbesar, dan beberapa aset utama yang digunakan pada setiap halaman, seperti gambar kucing menari (untuk navigasi) dan bayangan yang digunakan untuk semua gambar penulis. Namun, semuanya dimuat sebelumnya hanya jika pembaca kebetulan berada di layar yang lebih besar (>800px), meskipun kami mencari untuk menggunakan API Informasi Jaringan agar lebih akurat.
Kami juga telah mengurangi ukuran CSS penuh dan semua file CSS penting dengan menghapus kode lama, memfaktorkan ulang sejumlah komponen, dan menghapus trik text-shadow yang kami gunakan untuk mencapai garis bawah yang sempurna dengan kombinasi text-decoration-skip -tinta dan teks-dekorasi-tebal (akhirnya!).
Pekerjaan yang harus diselesaikan
Kami telah menghabiskan banyak waktu untuk mengerjakan semua perubahan kecil dan besar di situs. Kami telah melihat peningkatan yang cukup signifikan di desktop dan peningkatan yang cukup nyata di seluler. Saat penulisan, artikel kami mendapat skor rata-rata antara 90 dan 100 skor Lighthouse di desktop, dan sekitar 65-80 di seluler .
Alasan skor buruk di seluler jelas adalah Waktu untuk Interaktif yang buruk dan waktu Pemblokiran Total yang buruk karena aplikasi booting dan ukuran file CSS lengkap. Jadi masih ada beberapa pekerjaan yang harus dilakukan di sana.
Untuk langkah selanjutnya, kami sedang mencari cara untuk lebih mengurangi ukuran CSS , dan secara khusus memecahnya menjadi modul, mirip dengan JavaScript, memuat beberapa bagian CSS (misalnya checkout atau papan pekerjaan atau buku/eBuku) hanya ketika diperlukan.
Kami juga mengeksplorasi opsi eksperimen bundling lebih lanjut di seluler untuk mengurangi dampak kinerja app.js meskipun tampaknya tidak sepele saat ini. Terakhir, kita akan mencari alternatif untuk solusi cepat cookie kita, membangun kembali wadah kita dengan CSS clamp()
, mengganti teknik rasio padding-bottom dengan rasio aspect-ratio
dan mencari untuk menyajikan gambar sebanyak mungkin di AVIF.
Itu saja, Teman-teman!
Semoga studi kasus kecil ini bermanfaat bagi Anda, dan mungkin ada satu atau dua teknik yang dapat Anda terapkan pada proyek Anda segera. Pada akhirnya, kinerja adalah semua tentang jumlah dari semua detail kecil yang bagus, yang, ketika ditambahkan, membuat atau menghancurkan pengalaman pelanggan Anda.
Meskipun kami sangat berkomitmen untuk meningkatkan kinerja, kami juga berupaya meningkatkan aksesibilitas dan konten situs. Jadi, jika Anda menemukan sesuatu yang kurang tepat atau apa pun yang dapat kami lakukan untuk lebih meningkatkan Smashing Magazine, beri tahu kami di komentar di artikel ini.
Terakhir, jika Anda ingin terus mengikuti perkembangan artikel seperti ini, silakan berlangganan buletin email kami untuk mendapatkan tips ramah web, barang, alat dan artikel, dan pilihan musiman Smashing Cats.