Polling bertanya berulang kali "ada yang baru?". SSE (Server-Sent Events) membalik arahnya: browser membuka satu koneksi yang dibiarkan terbuka, dan server mendorong kabar lewat koneksi itu begitu ada perubahan. WebSocket juga satu koneksi terbuka, tapi dua arah: browser dan server sama-sama bisa mengirim kapan saja. Bayangin pelanggan Laundry Bu Rini. Polling itu pelanggan yang menelepon tiap 30 menit, "cucian saya sudah?". SSE itu pelanggan yang berlangganan pesan dari Bu Rini, dan pesannya datang tepat saat statusnya berubah. WebSocket itu telepon yang dibiarkan tersambung, dan dua-duanya bisa bicara.
🧪 Tebakan cepat: Papan memakai
hx-trigger="every 30s", dibuka pukul 08.00.00. Bu Rini mengubah #12 jadi Disetrika pukul 08.00.04. Kapan papan menampilkannya? Dan kalau papannya memakai SSE? Jawabannya ada di bagian "Polling atau SSE: kapan kabarnya sampai".
Aliran SSE: satu koneksi, banyak pesan
Browser meminta GET /papan/stream dan server membalas dengan Content-Type: text/event-stream tanpa menutup koneksinya. Setiap ada perubahan, server menulis satu pesan:
id: 1
event: order-12
data: <li id="order-12" sse-swap="order-12" hx-swap="outerHTML">#12 Pak Darto, 3 kg: <span>Disetrika</span></li>
id: 2
event: orderBaru
data: <li id="order-15" sse-swap="order-15" hx-swap="outerHTML">#15 Bu Tini, 4 kg: <span>Antre</span></li>
event:nama pesannya. Pesan tanpa barisevent:bernamamessage.data:isinya, di sini HTML. Tulis HTML-nya dalam satu baris.- Baris kosong menutup satu pesan.
id:nomor pesan, dipakai saat menyambung ulang.
Extension sse: sse-connect dan sse-swap
<script src="https://unpkg.com/[email protected]"></script>
<div hx-ext="sse" sse-connect="/papan/stream">
<ul id="papan-order" sse-swap="orderBaru" hx-swap="beforeend">
<li id="order-12" sse-swap="order-12" hx-swap="outerHTML">#12 Pak Darto, 3 kg: <span>Dicuci</span></li>
<li id="order-13" sse-swap="order-13" hx-swap="outerHTML">#13 Mbak Wulan, 2 kg: <span>Disetrika</span></li>
</ul>
<span hx-get="/jumlah" hx-trigger="sse:orderBaru">3 order hari ini</span>
</div>
hx-ext="sse"dansse-connect="/papan/stream"di pembungkus: satu koneksi untuk semua isinya.sse-swap="order-12": saat pesan bernamaorder-12tiba,data-nya ditukar ke elemen ini. BawaannyainnerHTML. Di sinihx-swap="outerHTML", jadi seluruh baris diganti baris baru dari server. Beberapa nama boleh dipisah koma.sse-swap="orderBaru"denganhx-swap="beforeend"di<ul>menambah baris baru di akhir papan. Baris #15 itu membawasse-swap="order-15"-nya sendiri, dan htmx langsung mendaftarkannya.hx-swapdiwariskan. Tanpahx-swap="outerHTML"sendiri, baris di dalam<ul>ikut memakaibeforeend, dan status barunya malah ditempel di belakang yang lama: "DicuciDisetrika".hx-trigger="sse:orderBaru": pesan itu memicu request htmx biasa, di siniGET /jumlah.sse-close="tutup"di pembungkus menutup koneksi saat server mengirim pesantutup. Kalau htmx mengganti atau menghapus pembungkusnya, koneksinya ikut ditutup.
Di Laravel 13, response()->eventStream() menulis format itu untukmu:
use Illuminate\Http\StreamedEvent;
use Illuminate\Support\Str;
Route::get('/papan/stream', function () {
return response()->eventStream(function () {
$sejak = now();
while (true) {
$cek = now();
foreach (Order::where('updated_at', '>', $sejak)->get() as $order) {
$baris = view('papan._baris', ['order' => $order])->render();
yield new StreamedEvent(event: "order-{$order->id}", data: Str::squish($baris));
}
$sejak = $cek;
sleep(1);
}
});
});
Str::squish() meratakan HTML jadi satu baris. Nilai yang di-yield tanpa StreamedEvent diberi nama update, bukan message.
Polling atau SSE: kapan kabarnya sampai
every 30s |
SSE | |
|---|---|---|
| #12 berubah 08.00.04, tampil di papan | 08.00.30, telat 26 detik | 08.00.04, langsung |
| Request per jam, satu tab | 120, kebanyakan isinya sama | 1 koneksi yang terus terbuka |
Polling menampilkannya pukul 08.00.30, pas giliran bertanya berikutnya. SSE menampilkannya saat itu juga. Itu jawaban tebakannya.
Kalau koneksinya putus
- Jaringan putus sebentar.
EventSourcemilik browser menyambung ulang sendiri beberapa detik kemudian (server bisa mengaturnya dengan barisretry: 3000). Kalau pesannya ber-id:, browser mengirim headerLast-Event-ID: 2saat menyambung, dan server bisa mengirim ulang pesan sesudah nomor 2 yang terlewat. - Browser menyerah, misalnya server membalas error atau
Content-Type-nya salah. Extension sse mencoba lagi sendiri dengan jeda yang terus berlipat: 0,5 detik, 1, 2, 4, dan seterusnya sampai paling lama 64 detik. Begitu tersambung, jedanya kembali dari awal. - Pesan yang terjadi selama putus dan gak dikirim ulang hilang.
eventStream()Laravel cuma menulisevent:dandata:, tanpaid:. Cara paling aman: setiap koneksi baru, server mengirim isi papan lengkap sebagai pesan pertama.
WebSocket: dua arah
<script src="https://unpkg.com/[email protected]"></script>
<div hx-ext="ws" ws-connect="/papan/ws">
<ul id="pesan"></ul>
<form ws-send>
<input name="pesan" placeholder="Pesan ke kurir">
<button>Kirim</button>
</form>
</div>
ws-connectmembuka WebSocket (ws://atauwss://). Alamat yang diawali/disesuaikan sendiri.- Setiap pesan dari server adalah HTML. Elemen ber-
iddi dalamnya menggantikan elemen dengan id yang sama, seperti OOB swap:<ul id="pesan" hx-swap-oob="beforeend"><li>Kurir: sudah di jalan</li></ul>. ws-senddi form mengirim isinya sebagai JSON lewat koneksi itu:{"pesan":"...","HEADERS":{...}}.- Kalau putus tak wajar, extension ws menyambung ulang dengan jeda acak yang terus membesar sampai 64 detik. Pesan yang dikirim selama putus menunggu dan dikirim begitu tersambung.
Kapan polling sudah cukup
- Polling cocok kalau telat beberapa detik sampai setengah menit gak masalah. Jalan di hosting mana pun, tanpa koneksi yang menggantung.
- SSE untuk kabar satu arah yang harus muncul saat itu juga, misalnya papan di TV toko atau notifikasi.
- WebSocket kalau browser juga sering mengirim, misalnya chat kasir dan kurir.
- Hitung biayanya. Di Laravel dengan PHP-FPM biasa, setiap koneksi SSE yang terbuka memegang satu worker PHP selama ia terbuka: 10 worker, 10 tab papan, pengunjung ke-11 menunggu. Di HTTP/1.1, browser juga cuma membuka 6 koneksi ke satu domain, termasuk koneksi SSE di tab lain. HTTP/2 gak punya batas itu.
⚠️ Jebakan yang sering ketemu
- Lupa
hx-ext="sse"atau script-nya.sse-connectcuma atribut biasa, gak ada koneksi. Di latihan, pembungkusnya butuhhx-ext="sse"dansse-connect="/events", dan<div>di dalamnya butuhsse-swap="message". - Nama pesan gak cocok.
sse-swap="message"gak menangkap pesan bernamaupdatedarieventStream()Laravel. Samakan namanya. hx-swap="beforeend"yang diwariskan. Baris di dalam<ul>ikut menambah, bukan mengganti. Beri tiap barishx-swap="outerHTML"sendiri.- HTML berbaris-baris di
data:. Baris kedua gak diawalidata:, jadi pesannya terpotong. Kirim satu baris.
TL;DR: SSE (Server-Sent Events) adalah satu koneksi satu arah, dari server ke browser, dengan pesan event:, data:, dan id: dipisah baris kosong. WebSocket dua arah. Dengan extension sse: hx-ext="sse" dan sse-connect="/papan/stream" di pembungkus, sse-swap="order-12" (atau message untuk pesan tanpa nama) di elemen penerima, hx-trigger="sse:orderBaru" untuk memicu request, dan sse-close untuk menutup. Putus sebentar: browser menyambung ulang dan mengirim Last-Event-ID. Browser menyerah: extension mencoba lagi dengan jeda 0,5, 1, 2, 4 detik sampai 64 detik. Dengan extension ws: ws-connect, pesan HTML ditukar menurut id, dan ws-send mengirim form sebagai JSON. every 30s bisa telat sampai 30 detik, SSE langsung. Polling cukup kalau telat gak masalah, dan ingat SSE memegang satu worker PHP per tab.