Di luar ekspresi reguler: Meningkatkan penguraian nilai CSS di Chrome DevTools

Philip Pfaffe
Ergün Erdogmus
Ergün Erdogmus

Pernahkah Anda memperhatikan properti CSS di Chrome DevTools' Gaya terlihat sedikit lebih bagus akhir-akhir ini? Update ini, yang diluncurkan antara Chrome 121 dan 128, adalah hasil dari peningkatan yang signifikan dalam cara kami mengurai dan menyajikan nilai CSS. Dalam artikel ini, kami akan menjelaskan detail teknis transformasi ini—berpindah dari sistem pencocokan ekspresi reguler ke parser yang lebih andal.

Mari bandingkan DevTools saat ini dengan versi sebelumnya:

Atas: ini adalah Chrome terbaru, Bawah: Chrome 121.

Cukup berbeda, bukan? Berikut ini perincian penyempurnaan utama:

  • color-mix. Pratinjau praktis yang secara visual mewakili dua argumen warna dalam fungsi color-mix.
  • pink. Pratinjau warna yang dapat diklik untuk warna yang diberi nama pink. Klik tombol tersebut untuk membuka pemilih warna agar mudah disesuaikan.
  • var(--undefined, [fallback value]). Peningkatan penanganan variabel yang tidak ditentukan, dengan variabel yang tidak ditentukan berwarna abu-abu dan nilai penggantian aktif (dalam hal ini, warna HSL) yang ditampilkan dengan pratinjau warna yang dapat diklik.
  • hsl(…): Pratinjau warna lain yang dapat diklik untuk fungsi warna hsl, yang memberikan akses cepat ke pemilih warna.
  • 177deg: Jam sudut yang dapat diklik yang memungkinkan Anda menarik dan mengubah nilai sudut secara interaktif.
  • var(--saturation, …): Link yang dapat diklik ke definisi properti kustom, yang memudahkan pengguna untuk menuju ke pernyataan yang relevan.

Perbedaannya sangat mencolok. Untuk mencapai hal ini, kita harus mengajari DevTools untuk memahami nilai properti CSS dengan jauh lebih baik daripada sebelumnya.

Bukankah pratinjau ini sudah tersedia?

Meskipun ikon pratinjau ini mungkin tampak familier, ikon tersebut tidak selalu ditampilkan secara konsisten, terutama dalam sintaksis CSS yang kompleks seperti contoh di atas. Bahkan saat mereka berhasil bekerja, upaya yang signifikan sering kali diperlukan agar aplikasi dapat berfungsi dengan baik.

Alasannya adalah karena sistem untuk menganalisis nilai telah berkembang secara organik sejak hari-hari pertama DevTools. Namun, mereka belum mampu mengikuti fitur baru menakjubkan yang baru-baru ini kami dapatkan dari CSS, serta peningkatan kompleksitas bahasa. Sistem ini perlu didesain ulang sepenuhnya untuk mengikuti perkembangan dan itulah yang kami lakukan!

Cara pemrosesan nilai properti CSS

Di DevTools, proses rendering dan dekorasi deklarasi properti di tab Gaya dibagi menjadi dua fase berbeda:

  1. Analisis struktural. Fase awal ini membedah deklarasi properti untuk mengidentifikasi komponen yang mendasarinya dan hubungannya. Misalnya, dalam deklarasi border: 1px solid red, deklarasi akan mengenali 1px sebagai panjang, solid sebagai string, dan red sebagai warna.
  2. Merender. Dibangun berdasarkan analisis struktural, fase {i>rendering<i} mengubah komponen-komponen ini menjadi representasi HTML. Hal ini akan memperkaya teks properti yang ditampilkan dengan elemen interaktif dan petunjuk visual. Misalnya, nilai warna red dirender dengan ikon warna yang dapat diklik yang, saat diklik, menampilkan pemilih warna untuk memudahkan modifikasi.

Ekspresi reguler

Sebelumnya, kita mengandalkan ekspresi reguler (regexe) untuk membedah nilai properti untuk analisis struktural. Kami mengelola daftar regex agar cocok dengan bit nilai properti yang kami anggap sebagai dekorasi. Misalnya, ada ekspresi yang cocok dengan warna, panjang, sudut, sub-ekspresi yang lebih rumit, seperti panggilan fungsi var, dan sebagainya. Kami memindai teks dari kiri ke kanan untuk melakukan analisis nilai, terus mencari ekspresi pertama dari daftar yang cocok dengan potongan teks berikutnya.

Meskipun sering kali bekerja dengan baik, jumlah kasus yang tidak terus bertambah. Selama bertahun-tahun kami telah menerima banyak laporan bug yang pencocokannya kurang tepat. Ketika kami memperbaikinya – beberapa perbaikan sederhana, sebagian lainnya cukup rumit – kami harus memikirkan kembali pendekatan kami untuk mencegah utang teknis kami. Mari kita lihat beberapa masalahnya.

Cocok dengan color-mix()

Ekspresi reguler yang kita gunakan untuk fungsi color-mix() adalah sebagai berikut:

/color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g

Yang cocok dengan sintaksnya:

color-mix(<color-interpolation-method>, [<color> && <percentage [0,100]>?]#{2})

Coba jalankan contoh berikut untuk memvisualisasikan kecocokan.

const re = /color-mix\(.*,\s*(?<firstColor>.+)\s*,\s*(?<secondColor>.+)\s*\)/g;

// it works - simpler example
const simpler = re.exec('color-mix(in srgb, pink, hsl(127deg 100% 50%))');
console.table(simpler.groups);

re.exec('');

// it doesn't work - complex example
const complex = re.exec('color-mix(in srgb, pink, var(--undefined, hsl(127deg var(--saturation, 100%) 50%)))');
console.table(complex.groups);

Hasil pencocokan untuk fungsi campuran warna.

Contoh yang lebih sederhana sudah cukup. Namun, dalam contoh yang lebih kompleks, pencocokan <firstColor> adalah hsl(177deg var(--saturation dan pencocokan <secondColor> adalah 100%) 50%)), yang sama sekali tidak bermakna.

Kami tahu ini adalah masalah. Bagaimanapun, CSS sebagai bahasa formal bukan reguler, jadi kami sudah menyertakan penanganan khusus untuk menangani argumen fungsi yang lebih rumit, seperti fungsi var. Namun, seperti yang Anda lihat di screenshot pertama, ini masih tidak berfungsi pada umumnya.

Cocok dengan tan()

Salah satu bug yang dilaporkan lebih lucu adalah tentang fungsi trigonometri tan() . Ekspresi reguler yang kami gunakan untuk mencocokkan warna mencakup sub-ekspresi \b[a-zA-Z]+\b(?!-) untuk mencocokkan warna bernama seperti kata kunci red. Kemudian kami memeriksa apakah bagian yang cocok benar-benar memiliki nama warna, dan coba tebak, tan juga merupakan warna yang telah diberi nama. Jadi, kami salah menafsirkan ekspresi tan() sebagai warna.

Cocok dengan var()

Mari lihat contoh lain, fungsi var() dengan penggantian yang berisi referensi var() lainnya: var(--non-existent, var(--margin-vertical)).

Ekspresi reguler untuk var() akan cocok dengan nilai ini. Namun, kode ini akan berhenti mencocokkan pada tanda kurung penutup pertama. Jadi, teks di atas cocok sebagai var(--non-existent, var(--margin-vertical). Ini adalah batasan buku pelajaran dari pencocokan ekspresi reguler. Bahasa yang memerlukan tanda kurung yang cocok pada dasarnya tidak reguler.

Transisi ke parser CSS

Jika analisis teks yang menggunakan ekspresi reguler berhenti berfungsi (karena bahasa yang dianalisis tidak reguler), ada langkah kanonis berikutnya: gunakan parser untuk tata bahasa jenis yang lebih tinggi. Untuk CSS, hal ini berarti parser untuk bahasa bebas konteks. Sebenarnya, sistem parser tersebut sudah ada di codebase DevTools: Lezer CodeMirror, yang merupakan fondasi, misalnya, penyorotan sintaksis di CodeMirror, editor yang Anda temukan di panel Sources. Parser CSS Lezer memungkinkan kita menghasilkan hierarki sintaksis (non-abstrak) untuk aturan CSS dan siap digunakan. Kemenangan.

Hierarki sintaksis untuk nilai properti `hsl(177deg var(--saturation, 100%) 50%)`. Ini adalah versi sederhana dari hasil yang dihasilkan oleh parser Lezer, dengan menyisakan node sintaksis yang murni untuk koma dan tanda kurung.

Namun, secara langsung, kami merasa tidak mungkin bermigrasi dari pencocokan berbasis ekspresi reguler ke pencocokan berbasis parser secara langsung: kedua pendekatan ini bekerja dari arah yang berlawanan. Saat mencocokkan potongan nilai dengan ekspresi reguler, DevTools akan memindai input dari kiri ke kanan, berulang kali mencoba menemukan kecocokan paling awal dari daftar pola yang diurutkan. Dengan hierarki sintaksis, pencocokan akan dimulai dari bawah ke atas, misalnya, menganalisis argumen panggilan terlebih dahulu, sebelum mencoba mencocokkan panggilan fungsi. Anggap saja sebagai mengevaluasi ekspresi aritmetika, di mana Anda pertama-tama akan mempertimbangkan ekspresi yang diberi tanda kurung, lalu operator perkalian, lalu operator penjumlahan. Dalam framing ini, pencocokan berbasis ekspresi reguler sesuai dengan mengevaluasi ekspresi aritmetika dari kiri ke kanan. Kami sebenarnya tidak ingin menulis ulang seluruh sistem pencocokan dari awal: Ada 15 pasangan matcher dan perender yang berbeda, dengan ribuan baris kode, yang membuat kami tidak mungkin mengirimkannya dalam satu tonggak pencapaian.

Jadi, kami menemukan solusi yang memungkinkan kami membuat perubahan tambahan, yang akan kami jelaskan secara lebih mendetail di bawah ini. Singkatnya, kami mempertahankan pendekatan dua fase, tetapi pada fase pertama kami mencoba mencocokkan sub-ekspresi dari bawah ke atas (sehingga dipisahkan dengan alur ekspresi reguler), dan pada fase kedua kami merender dari atas ke bawah. Dalam kedua fase tersebut, kita dapat menggunakan matcher dan render berbasis ekspresi reguler yang sudah ada, praktis tanpa perubahan, dan dengan demikian dapat memigrasikannya satu per satu.

Tahap 1: Pencocokan {i>bottom-up<i} (dari bawah ke atas)

Fase pertama kurang lebih persis dan secara eksklusif melakukan apa yang tertera di sampul. Kami melintasi pohon secara berurutan dari bawah ke atas dan mencoba mencocokkan sub-ekspresi pada setiap node hierarki sintaksis yang kami kunjungi. Untuk mencocokkan sub-ekspresi tertentu, matcher dapat menggunakan ekspresi reguler seperti pada sistem yang ada. Mulai versi 128, kami sebenarnya masih melakukannya dalam beberapa kasus, misalnya, untuk panjang yang cocok. Atau, matcher dapat menganalisis struktur subhierarki yang di-root pada node saat ini. Hal ini memungkinkannya untuk menangkap {i>error <i}pada {i>syntax<i} dan merekam informasi struktural secara bersamaan.

Pertimbangkan contoh hierarki sintaksis dari atas:

Fase 1: Pencocokan {i>bottom-up<i} pada hierarki sintaks.

Untuk pohon ini, matcher kita akan diterapkan dalam urutan berikut:

  1. hsl(177degvar(--saturation, 100%) 50%): Pertama, kita menemukan argumen pertama dari panggilan fungsi hsl, yaitu sudut hue. Kami mencocokkannya dengan pencocok sudut, sehingga kami dapat mendekorasi nilai sudut dengan ikon sudut.
  2. hsl(177degvar(--saturation, 100%)50%): Kedua, kita menemukan panggilan fungsi var dengan var matcher. Untuk panggilan seperti itu, sebaiknya kita melakukan dua hal:
    • Cari deklarasi variabel dan hitung nilainya, serta tambahkan link dan pop-up ke nama variabel untuk menghubungkannya.
    • Hiasi panggilan dengan ikon warna jika nilai yang dihitung adalah warna. Sebenarnya ada hal ketiga, tapi kita akan membahasnya nanti.
  3. hsl(177deg var(--saturation, 100%) 50%): Terakhir, kita cocokkan ekspresi panggilan untuk fungsi hsl, sehingga dapat mendekorasinya dengan ikon warna.

Selain menelusuri sub-ekspresi yang ingin kita hias, sebenarnya ada fitur kedua yang kita jalankan sebagai bagian dari proses pencocokan. Perhatikan bahwa pada langkah #2, kita mencari nilai komputasi untuk sebuah nama variabel. Bahkan, kita mengambil satu langkah lebih jauh dan menyebarkan hasilnya ke atas hierarki. Dan bukan hanya untuk variabel, tetapi juga untuk nilai penggantian. Saat mengunjungi node fungsi var, turunannya telah dikunjungi sebelumnya, sehingga kita sudah mengetahui hasil dari setiap fungsi var yang mungkin muncul dalam nilai penggantian. Oleh karena itu, kami dapat dengan mudah dan murah mengganti fungsi var dengan hasilnya dengan cepat, yang memungkinkan kita dengan mudah menjawab pertanyaan seperti "Apakah hasil var ini memanggil warna?", seperti yang kita lakukan pada langkah #2.

Tahap 2: Rendering top-down

Untuk fase kedua, kita membalikkan arah. Dengan mengambil hasil yang cocok dari fase 1, kita merender pohon menjadi HTML dengan menelusurinya secara berurutan dari atas ke bawah. Untuk setiap node yang dikunjungi, kita memeriksa apakah node tersebut cocok. Jika ya, panggil perender yang sesuai dari matcher. Kita menghindari kebutuhan penanganan khusus untuk node yang hanya berisi teks (seperti NumberLiteral "50%") dengan menyertakan pencocok dan perender default untuk node teks. Perender hanya menghasilkan node HTML, yang jika disatukan, menghasilkan representasi nilai properti termasuk dekorasinya.

Fase 2: Rendering top-down pada hierarki sintaksis.

Untuk contoh hierarki, berikut adalah urutan nilai properti dirender:

  1. Kunjungi panggilan fungsi hsl. Warnanya cocok, jadi panggil perender fungsi warna. Fungsi ini melakukan dua hal:
    • Menghitung nilai warna sebenarnya menggunakan mekanisme substitusi sambil berjalan untuk argumen var apa pun, lalu menggambar ikon warna.
    • Merender turunan CallExpression secara rekursif. Ini secara otomatis menangani rendering nama fungsi, tanda kurung, dan koma, yang hanya merupakan teks.
  2. Kunjungi argumen pertama panggilan hsl. Ini cocok, jadi panggil perender sudut, yang menggambar ikon sudut dan teks sudut.
  3. Kunjungi argumen kedua, yaitu panggilan var. Karena cocok, panggil var renderer yang menghasilkan output berikut:
    • Teks var( di awal.
    • Nama variabel dan mendekorasinya dengan tautan ke definisi variabel atau dengan warna teks abu-abu untuk menunjukkannya tidak didefinisikan. Ini juga menambahkan pop-up ke variabel untuk menampilkan informasi tentang nilainya.
    • Koma, lalu secara rekursif merender nilai penggantian.
    • Tanda kurung tutup.
  4. Kunjungi argumen terakhir panggilan hsl. Tidak cocok, jadi cukup output isi teksnya.

Apakah Anda memperhatikan bahwa dalam algoritme ini, sebuah {i>render<i} mengontrol sepenuhnya bagaimana turunan dari {i>node<i} yang cocok dirender? Merender turunan secara rekursif bersifat proaktif. Trik ini memungkinkan migrasi bertahap dari rendering berbasis ekspresi reguler ke rendering berbasis hierarki sintaksis. Untuk node yang cocok dengan ekspresi reguler lama, perender yang sesuai dapat digunakan dalam bentuk aslinya. Dalam istilah hierarki sintaks, ia akan bertanggung jawab untuk merender seluruh subpohon, dan hasilnya (node HTML) dapat dicolokkan dengan bersih ke dalam proses rendering di sekitarnya. Ini memberi kita opsi untuk melakukan port matcher dan perender berpasangan, lalu menukarnya satu per satu.

Fitur keren lainnya dari perender yang mengontrol rendering turunan node yang cocok adalah hal ini memberikan kemampuan untuk menjelaskan dependensi di antara ikon yang ditambahkan. Dalam contoh di atas, warna yang dihasilkan oleh fungsi hsl jelas bergantung pada nilai hue-nya. Artinya warna yang ditampilkan oleh ikon warna bergantung pada sudut yang ditunjukkan oleh ikon sudut. Jika pengguna membuka editor sudut melalui ikon tersebut dan mengubah sudut, sekarang kita dapat memperbarui warna ikon warna secara real time:

Seperti yang dapat Anda lihat pada contoh di atas, kita juga menggunakan mekanisme ini untuk penyambungan ikon lainnya, seperti untuk color-mix() dan dua saluran warnanya, atau fungsi var yang menampilkan warna dari penggantiannya.

Dampak performa

Saat terjun ke masalah ini untuk meningkatkan keandalan dan memperbaiki masalah yang sudah lama ada, kami memperkirakan beberapa regresi performa mengingat bahwa kami mulai menjalankan parser yang lengkap. Untuk mengujinya, kami telah membuat benchmark yang merender sekitar 3,5 ribu deklarasi properti dan membuat profil versi berbasis ekspresi reguler dan berbasis parser dengan throttling 6x pada mesin M1.

Seperti yang kami perkirakan, pendekatan berbasis penguraian ternyata 27% lebih lambat daripada pendekatan berbasis ekspresi reguler untuk kasus tersebut. Pendekatan berbasis ekspresi reguler membutuhkan waktu 11 detik untuk dirender dan pendekatan berbasis parser memerlukan waktu 15 detik untuk dirender.

Mempertimbangkan keunggulan yang kami dapatkan dari pendekatan baru, kami memutuskan untuk melanjutkannya.

Ucapan terima kasih

Kami sangat berterima kasih kepada Sofia Emelianova dan Jecelyn Yeen atas bantuannya yang sangat berharga dalam mengedit postingan ini.

Mendownload saluran pratinjau

Pertimbangkan untuk menggunakan Chrome Canary, Dev, atau Beta sebagai browser pengembangan default Anda. Saluran pratinjau ini memberi Anda akses ke fitur DevTools terbaru, menguji API platform web mutakhir, dan menemukan masalah di situs Anda sebelum pengguna melakukannya.

Menghubungi tim Chrome DevTools

Gunakan opsi berikut untuk membahas fitur dan perubahan baru dalam postingan, atau hal lain yang terkait dengan DevTools.

  • Kirim saran atau masukan kepada kami melalui crbug.com.
  • Laporkan masalah DevTools menggunakan Opsi lainnya   Lainnya > Bantuan > Laporkan masalah DevTools di DevTools.
  • Tweet di @ChromeDevTools.
  • Tulis komentar di Video YouTube yang baru di DevTools atau Tips DevTools Video YouTube.