D
P
0
← Semua artikel Read in English

Jebakan Build-Time Tailwind v4

Breakpoint Tailwind 4 Tidak Jalan di Safari Lama Sesudah Pindah ke Astro? `(width>=40rem)` Dinaikkan Lagi oleh Minifier

· · 6 menit baca
Breakpoint Tailwind 4 Tidak Jalan di Safari Lama Sesudah Pindah ke Astro? `(width>=40rem)` Dinaikkan Lagi oleh Minifier

Versi Tailwind di situs portofolio saya tidak bergeser sedikit pun waktu situsnya saya pindahkan dari Next.js ke Astro. Proyek Astro-nya mem-pin Tailwind 4.2.1 dan Astro 7.3.1, dan setiap versi yang di-pin di sana cocok dengan versi yang ter-resolve di build Next yang sedang tayang. Kompilernya sama persis, tapi CSS hasil build-nya tidak: perpindahan itu diam-diam menaikkan lantai dukungan peramban ke Safari 16.4.

Dampaknya bukan kosmetik. Pesan commit perbaikannya mencatat sepuluh blok breakpoint yang gagal utuh di Safari 16.3 ke bawah, dan yang rusak adalah tata letak, bukan warna. Cacat ini satu dari enam yang ditemukan dan ditambal di tahap paritas migrasi, dan termasuk tiga yang saya catat sebagai paling layak diingat. Tambalannya masuk 8 September 2026, sebelum domain dipindah ke build Astro.

Tailwind 4 menulis breakpoint dengan sintaks rentang

Di Tailwind 4.2.1, varian breakpoint disusun dari namespace --breakpoint di tema, dan setiap varian dibungkus @media bersintaks rentang dengan bentuk (width >= nilai). Tema bawaannya mengisi sm dengan 40rem, md 48rem, lg 64rem, xl 80rem, dan 2xl 96rem. Kelas seperti lg:grid-cols-2 akhirnya kira-kira berbentuk seperti blok pertama di bawah, sedangkan peramban lama butuh bentuk kedua:

/* disederhanakan: keluaran Tailwind 4 untuk lg:grid-cols-2 */
@media (width >= 64rem) {
  .lg\:grid-cols-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}
 
/* bentuk lama yang setara */
@media (min-width: 64rem) {
  .lg\:grid-cols-2 {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
}

Sintaks rentang baru dikenal Safari sejak versi 16.4. Peramban yang lebih tua tidak membaca (width >= 64rem) sebagai kondisi yang sedikit keliru, melainkan sebagai kondisi yang tidak bisa dipahami, sehingga seluruh blok dianggap tidak cocok dan semua aturan di dalamnya dilewati. Itulah arti gagal utuh. Di stylesheet mobile-first seperti keluaran Tailwind, kelas tanpa awalan berlaku di semua lebar, sementara kelas berawalan breakpoint hanya hidup di dalam blok-blok itu. Kalau bloknya tidak pernah cocok, layar lebar pun hanya mendapat aturan untuk layar terkecil.

Warna menempuh jalur yang mirip. Tailwind 4.2.1 mengubah nilai alfa menjadi persen lalu mencampur warnanya dengan transparent lewat color-mix(in oklab, ...), kecuali alfanya 100%. Build Next kebetulan menurunkan sintaks rentang dan oklab() sekaligus. Build Astro membiarkan keduanya: sebelum diperbaiki, CSS berandanya memuat 16 sintaks rentang dan 21 oklab atau oklch.

Bagian pertama: target lama untuk transformer

Astro membangun dengan Vite, dan Tailwind di situs ini masuk lewat plugin @tailwindcss/vite di vite.plugins. Jadi tempat menurunkan sintaksnya ada di kunci vite yang sama: css.transformer disetel ke 'lightningcss', lalu css.lightningcss.targets diisi peramban lama, yaitu Safari dan Safari iOS 15 serta Chrome, Firefox, dan Edge 100. Versinya ditulis seperti (15 << 16) karena lightningcss membaca versi peramban sebagai satu angka. Vite sendiri menyandikannya dengan cara yang sama saat menyiapkan target untuk lightningcss: angka mayor digeser 16 bit dan minor 8 bit, jadi 15 << 16 berarti 15.0.

Percobaan pertama berhenti setengah jalan. Warnanya sudah turun, tapi media query belum, dan baru turun sesudah minifier dipisah. Perbaikan ini akhirnya punya dua bagian, dan bagian kedua, yaitu minifier, yang menggagalkan percobaan pertama.

Minifier bawaan memakai targetnya sendiri

Pesan commit-nya merangkum masalah ini dalam satu kalimat: transformer lightningcss dengan target peramban lama menurunkan sintaksnya, tapi minifier bawaan menaikkannya lagi karena dia memakai targetnya sendiri. Asal target itu bisa dirunut di kode Astro 7.3.1 dan Vite 8.2.2 yang terpasang. Potongan di bawah sudah dipangkas ke baris yang relevan:

// astro 7.3.1, konfigurasi build yang disusun untuk Vite (dipangkas)
build: {
  target: "esnext",
  cssMinify: viteConfig.build?.minify == null ? true : !!viteConfig.build?.minify,
  ...viteConfig.build,
},
 
// vite 8.2.2 (dipangkas)
cssTarget: merged.cssTarget ?? merged.target,
 
// di dalam minifyCSS
if (config.build.cssMinify === "esbuild") {
  // jalur esbuild
}
(await importLightningCSS()).transform({
  ...config.css.lightningcss,
  targets: convertTargets(config.build.cssTarget),
});
 
// di dalam convertTargets
if (entry === "esnext") continue;
const result = Object.keys(targets).length > 0 ? targets : void 0;

Rangkaiannya begini. Astro menulis target: "esnext" ke konfigurasi build Vite yang dia susun, di tingkat atas maupun di lingkungan client. Selama cssTarget tidak diisi, Vite memakai build.target itu sebagai cssTarget. Selama cssMinify bukan persis "esbuild", minifyCSS memanggil lightningcss dengan menyebar css.lightningcss milik saya lebih dulu, lalu menimpa targets dengan hasil convertTargets(config.build.cssTarget). Dan convertTargets melewati "esnext", sehingga tidak ada target yang tersisa dan nilai yang dikembalikan undefined.

Jadi begitu transformer disetel ke lightningcss dan minify dibiarkan bawaan, CSS melewati lightningcss dua kali. Di tahap transformer, compileLightningCSS menyebar css.lightningcss ke bundleAsync, sehingga target Safari 15 saya terpakai dan sintaksnya turun. Di tahap minify, target yang sama ditimpa undefined. Minifier itu tidak lagi diberi tahu peramban mana yang harus dilayani, dan menurut pesan commit, sintaks yang baru saja diturunkan dinaikkan lagi.

Tujuh baris di konfigurasi

Bagian kedua cukup satu baris, build: { cssMinify: 'esbuild' } di dalam kunci vite. Astro mengisi cssMinify dengan true kalau build.minify tidak diisi, lalu menyebar vite.build milik saya sesudahnya, jadi nilai dari saya yang menang. Dengan cssMinify bernilai persis "esbuild", minifyCSS mengambil jalur esbuild dan lightningcss tidak lagi dipanggil dengan target kosong. Konfigurasi akhirnya, dipangkas ke bagian yang relevan:

// astro.config.mjs (dipangkas)
import tailwindcss from '@tailwindcss/vite';
 
export default defineConfig({
  vite: {
    plugins: [tailwindcss()],
    css: {
      transformer: 'lightningcss',
      lightningcss: {
        targets: { safari: (15 << 16), chrome: (100 << 16), firefox: (100 << 16), edge: (100 << 16), ios_saf: (15 << 16) },
      },
    },
    build: { cssMinify: 'esbuild' },
  },
});

Commit perbaikannya hanya mengubah berkas itu, dengan tujuh baris tambahan, dan tidak menambah dependensi apa pun karena lightningcss sudah ada di pohon sebagai milik Vite. Lockfile pada commit itu mencatat Vite 8.2.2 di bawah Astro, dengan lightningcss ^1.33.0 di daftar dependensinya.

Yang terukur di CSS beranda

Diukur pada CSS beranda, sintaks rentang turun dari 16 menjadi 0, sedangkan oklab dan oklch turun dari 21 menjadi 0. Pada saat yang sama, min-width naik dari 3 menjadi 28, max-width tercatat 25, dan fallback hex naik dari 96 menjadi 145. Ukurannya naik dari 76.843 menjadi 77.989 byte. Selisihnya 77.989 dikurangi 76.843, yaitu 1.146 byte atau sekitar 1,1 KB, kira-kira 1,5 persen dari ukuran awal.

Karena menyentuh CSS seluruh 509 halaman sekaligus, perubahan ini diverifikasi tiga lapis sebelum di-push, dan pesan commit mencatat hasilnya. Dari 408 selektor yang ada di kedua versi, daftar properti 400 di antaranya identik, dan delapan sisanya berbeda karena Lightning CSS menambahkan sesuatu: -webkit-backdrop-filter yang justru dibutuhkan Safari, -webkit-appearance, dan set fallback warna. Jumlah blok @media tetap 13 lawan 13, dan tidak ada aturan yang hilang. Di peramban sungguhan pada lebar 1440px, breakpoint berlaku: lg:grid-cols-2 menghasilkan dua kolom 720px, persis separuh dari 1440, dan warna beralfa resolve ke nilai yang benar.

Yang belum terbukti

Ada batas yang perlu saya tulis terang-terangan. Catatan saya tidak menyebut uji di Safari 16.3 ke bawah yang sungguhan, baik untuk kerusakannya maupun untuk perbaikannya, dan peramban pada uji 1440px tidak tercatat nama maupun versinya. Yang terbukti adalah keluaran CSS-nya dan perilakunya di peramban modern: sintaks rentang hilang dari CSS beranda, jumlah blok @media tidak berubah, dan breakpoint tetap berlaku. Bahwa tata letaknya utuh di Safari lama yang sungguhan masih kesimpulan dari CSS, belum pengamatan.

Lantai peramban ikut pipeline, bukan versi Tailwind

Bug ini tidak akan ketemu lewat perbandingan versi paket, karena versinya memang sama. Yang berubah adalah apa yang dikerjakan build di sekitar Tailwind: build Next kebetulan menurunkan sintaksnya, build Astro tidak, dan ketika saya memintanya turun, minifier bawaan menaikkannya lagi. Kalau warnanya sudah turun tapi width>= masih ada di CSS hasil build, polanya sama dengan percobaan pertama saya: transformernya sudah bekerja, dan yang perlu dicurigai adalah minifiernya. Menghitung width>= dan oklab di CSS hasil build sebelum dan sesudah ganti build adalah cara murah untuk melihat apakah lantai peramban ikut bergeser.