Server Islands
ปัญหาที่ server islands แก้
หัวข้อที่มีชื่อว่า “ปัญหาที่ server islands แก้”client island เป็นเรื่องของ interactivity ฝั่ง client ส่วน server islands แก้ปัญหาอีกแบบ: หน้าที่ส่วนใหญ่ cache ได้และ static แต่มีส่วนหนึ่งที่ personalize หรือช้า — avatar ของ user ที่ login อยู่, แถบ “แนะนำสำหรับคุณ”, ราคาสด ๆ ที่ต้อง query ใหม่
ถ้าไม่มี server islands คุณจะถูกบังคับให้เลือก: render ทั้งหน้าแบบ on demand (เสีย cache ของ 95% ที่ไม่เคยเปลี่ยน) หรือยอมทิ้งส่วนที่ personalize server islands ให้คุณได้ทั้งคู่ — cache เปลือก แล้ว defer ส่วนที่ dynamic
server:defer
หัวข้อที่มีชื่อว่า “server:defer”เพิ่ม directive server:defer ให้ .astro component ตัวไหนก็ได้ เพื่อเปลี่ยน component นั้นเป็น server island จากนั้น Astro จะ render ส่วนที่เหลือของหน้าทันที — โดยไม่รอ component ตัวนั้น — แล้ว HTML จริงของ component ที่ defer ไว้จะถูกดึงมาแยกต่างหากและสลับเข้ามาเมื่อพร้อม
---import Avatar from '../components/Avatar.astro';---<Avatar server:defer />HTML เริ่มต้นของหน้าออกทันทีและ cache ที่ edge ได้ ส่วน Avatar render บน server — จึงอ่าน cookie, session หรือ database ได้อย่างปลอดภัย — แต่ทำ นอกรอบ บน request ของตัวเอง หลังจากเปลือกอยู่บนจอแล้ว
fallback content ด้วย slot="fallback"
หัวข้อที่มีชื่อว่า “fallback content ด้วย slot="fallback"”เพราะ content จริงมาช้ากว่าเล็กน้อย คุณจึงใส่ fallback content ให้แสดงระหว่างรอ — skeleton, spinner หรือ placeholder กลาง ๆ — ด้วย slot="fallback":
---import Avatar from '../components/Avatar.astro';import GenericAvatar from '../components/GenericAvatar.astro';---<Avatar server:defer> <GenericAvatar slot="fallback" /></Avatar>GenericAvatar render เข้าไปในเปลือกที่ cache ไว้ทันที ส่วน Avatar ที่ personalize จะเข้ามาแทนเมื่อ server render ของตัวเองส่งกลับมา
flowchart TB req["request the page"] --> shell["server renders shell instantly (cacheable)"] shell --> fb["fallback shown in place of the island"] fb --> fetch["browser fetches the deferred island"] fetch --> srv["island renders on the server (cookies, session, db)"] srv --> swap["real HTML swapped in over the fallback"]
HTML จริงที่ build ออกมา
หัวข้อที่มีชื่อว่า “HTML จริงที่ build ออกมา”fallback content, การ fetch และการสลับไม่ใช่มายากลของ framework ที่ซ่อนอยู่ — build หน้าจริงที่มี Avatar แบบ server:defer (fallback เป็น GenericAvatar) แล้วอ่าน output จริง เห็นทั้งสามอย่างครบ เรียงตามลำดับ เป็น HTML ธรรมดา:
<img src="/generic-avatar.png" alt="Generic avatar" width="48" height="48" />---const cookie = Astro.request.headers.get('cookie');const userId = cookie?.match(/uid=(\\w+)/)?.[1] ?? null;---<div data-testid="avatar-real"> {userId ? <p>Welcome back, user {userId}</p> : <p>Anonymous (no uid cookie forwarded)</p>}</div>---import Avatar from '../components/Avatar.astro';import GenericAvatar from '../components/GenericAvatar.astro';---<html><body><h1>Server island demo</h1><Avatar server:defer> <GenericAvatar slot="fallback" /></Avatar></body></html><!DOCTYPE html><html><link rel="preload" as="fetch" href="/_server-islands/Avatar?e=FDA01BAC8D95CB8E26A560DAB87l7Jmcc5evsrWx2DYJJRT%2BfX1KJFY%3D&p=&s=" crossorigin="anonymous"><body><h1>Server island demo</h1><script>async function replaceServerIsland(id, r) { let s = document.querySelector(`script[data-island-id="${id}"]`); if (!s || r.status !== 200 || r.headers.get('content-type')?.split(';')[0].trim() !== 'text/html') return; let html = await r.text(); while (s.previousSibling && s.previousSibling.nodeType !== 8 && s.previousSibling.data !== '[if astro]>server-island-start<![endif]') s.previousSibling.remove(); s.previousSibling?.remove(); s.before(document.createRange().createContextualFragment(html)); s.remove(); }</script><!--[if astro]>server-island-start<![endif]--><img src="/generic-avatar.png" alt="Generic avatar" width="48" height="48"><script type="module" data-astro-rerun data-island-id="f07047c9-f26d-45fe-a9ea-1ac642bcc610">const headers = new Headers({});let response = await fetch("/_server-islands/Avatar?e=FDA01BAC8D95CB8E26A560DAB87l7Jmcc5evsrWx2DYJJRT%2BfX1KJFY%3D&p=&s=", { headers });replaceServerIsland("f07047c9-f26d-45fe-a9ea-1ac642bcc610", response);</script></body></html>สี่ชิ้นจริง เรียงตามลำดับที่ปรากฏจริง:
<link rel="preload" as="fetch">ใน<head>— browser เริ่ม fetch endpoint ของ server island ก่อนจะไปถึง body ด้วยซ้ำ โดยไม่บล็อก render- script runtime
replaceServerIslandที่ใช้ร่วมกัน — ประมาณ 15 บรรทัดเท่าเดิม ไม่ว่าหน้าจะมี server island กี่ตัว - markup fallback ของ
GenericAvatarอยู่ตรงตำแหน่งที่Avatarจะมาแทนที่ — นี่คือสิ่งที่ paint ก่อน <script type="module" data-island-id="...">ที่ใช้ URL เดิมกับที่ preload fetch ไว้ แล้วเรียกreplaceServerIsland(id, response)เพื่อสลับ fallback เป็น HTML จริงเมื่อมาถึง
การเข้ารหัส props
หัวข้อที่มีชื่อว่า “การเข้ารหัส props”props (และ slot content) ที่ส่งให้ server island ไม่ได้ถูกส่งไป browser แบบ plain text — URL ข้างบน (?e=...&p=&s=...) ถูกเข้ารหัสจริง ไม่ใช่แค่ obfuscate เอกสารจริงของ Astro (astro/dist/core/encryption.js) ใช้ AES-GCM ด้วย key 256-bit ที่ generate ใหม่ทุก build เก็บไว้ฝั่ง server; ตามเอกสารทางการ (docs.astro.build/en/guides/server-islands/#reusing-the-encryption-key):
Astro uses cryptography to encrypt props passed to server islands, protecting sensitive data from accidental exposure. This encryption relies on a new, random key that is generated on each build and embedded in the server bundle.
key ใหม่ทุก build แปลว่า CDN ที่ cache หน้าข้ามการ deploy หรือ rolling deployment ที่ server instance เก่ากับใหม่อยู่ด้วยกันชั่วคราว อาจเจอปัญหา decrypt ไม่ตรงกัน ทางแก้คือ astro create-key ที่ generate key คงที่ให้ตั้งเป็น environment variable ASTRO_KEY เพื่อให้ทุก build (และทุก backend instance) เข้ารหัส/ถอดรหัสด้วย key เดียวกัน
Cache header บน response — พิสูจน์แล้ว และไม่ใช่อย่างที่อาจคิด
หัวข้อที่มีชื่อว่า “Cache header บน response — พิสูจน์แล้ว และไม่ใช่อย่างที่อาจคิด”curl -D - จริงกับ endpoint _server-islands ที่ build ออกมา (header อย่างเดียว จาก build ที่รันจริง):
$ curl -s -D - "http://127.0.0.1:4601/_server-islands/Avatar?e=...&p=&s=" -o /dev/nullHTTP/1.1 200 OKcontent-type: text/htmlx-robots-tag: noindexDate: Sat, 26 Sep 2026 02:35:38 GMTConnection: keep-aliveKeep-Alive: timeout=5Transfer-Encoding: chunkedไม่มี Cache-Control header เลย โดย default — มีแค่ x-robots-tag: noindex (กัน search engine index endpoint fragment ดิบ ๆ) Astro ไม่ cache response ของ server island ให้เอง ถ้า fragment นั้น cache ได้ ต้องตั้ง Cache-Control เองข้างใน component (เช่น Astro.response.headers.set('Cache-Control', ...)) เหมือนกับ route on-demand อื่น ๆ
server islands ต้องมี adapter แม้แต่บนไซต์ที่ static ทั้งหมด
หัวข้อที่มีชื่อว่า “server islands ต้องมี adapter แม้แต่บนไซต์ที่ static ทั้งหมด”ข้อนี้ไม่ใช่คำแนะนำเบา ๆ — ถอด adapter ออกจากโปรเจกต์ output: 'static' ที่มี server:defer อยู่ที่ไหนสักที่ build จะพังทันที ตั้งแต่ก่อนจะเริ่ม prerender หน้าไหนด้วยซ้ำ:
[NoAdapterInstalledServerIslands] Cannot use server islands without an adapter. Please installand configure the appropriate server adapter for your final deployment. Hint: See https://docs.astro.build/en/guides/on-demand-rendering/ for more information. Error reference: https://docs.astro.build/en/reference/errors/no-adapter-installed-server-islands/(ทำซ้ำจริงกับ astro build ที่ output: 'static' ไม่มี adapter เลย มีหน้าเดียวที่ใช้ server:defer) เพิ่ม adapter: node({ mode: 'standalone' }) — หรือ adapter ไหนก็ได้ — กลับเข้าไป โปรเจกต์เดียวกันนี้ build ผ่านสะอาดทันที ไม่ต้องเปลี่ยนอะไรในหน้าเลย
Cookie ข้างใน server island
หัวข้อที่มีชื่อว่า “Cookie ข้างใน server island”fetch ของ server island รันแบบ same-origin จาก browser และ fetch() แบบ same-origin ส่ง cookie ให้โดย default (ไม่ต้องใส่ option credentials) — ดังนั้น Astro.request.headers.get('cookie') ข้างใน Avatar.astro เห็น cookie ของผู้เยี่ยมชมจริง พิสูจน์ด้วย browser session จริง: ตั้ง document.cookie = 'uid=99' แล้ว navigate ใหม่ไปหน้านั้น พอ fetch ที่ defer resolve:
document.querySelector('[data-testid=avatar-real]').outerHTML// "<div data-testid=\"avatar-real\"><p>Welcome back, user 99</p></div>"— เทียบกับตอนไม่มี cookie เลย ที่ได้ผลลัพธ์แบบ anonymous ทั่วไป ("Anonymous (no uid cookie forwarded)" พิสูจน์ก่อนหน้าด้วย curl ตรง ๆ ที่ไม่มี header Cookie) ไม่ต้องทำอะไรพิเศษเพื่อส่ง cookie เลย มันเป็นแค่ same-origin request ธรรมดาในมุมมองของ browser
ใช้เมื่อไหร่ (และเรื่อง adapter ที่ต้องมี)
หัวข้อที่มีชื่อว่า “ใช้เมื่อไหร่ (และเรื่อง adapter ที่ต้องมี)”server islands เป็นเครื่องมือที่ใช่เมื่อหน้าหนึ่ง cache ได้ ยกเว้นส่วน dynamic เล็ก ๆ:
- หน้า marketing หรือหน้าสินค้าที่เหมือนกันสำหรับทุกคน ยกเว้น คำทักทายที่ personalize หรือ cart
- หน้าที่ส่วนใหญ่ static แต่มี query ที่ ช้า อยู่หนึ่งอันที่คุณไม่อยากให้บล็อก first paint
- ส่วน auth-gated บนหน้าที่เป็น public และ CDN-cache
จำไว้สองอย่าง อย่างแรก server:defer render บน server จึงต้องติดตั้ง adapter (Cloudflare, Node, Vercel…) — เงื่อนไขเดียวกับ on-demand rendering ทุกแบบ ถ้าไม่มีจะเจอ NoAdapterInstalledServerIslands build error จริงที่พิสูจน์ข้างบน แม้บนโปรเจกต์ที่ไม่มี adapter และ static ทั้งหมด อย่างที่สอง ไม่ใช่สำหรับ client interactivity — ถ้าส่วนนั้นต้องตอบสนอง click นั่นคือ client island (client directive) อาจต้องใช้เพิ่มเติม server island เป็นเรื่องของการ defer output ที่ render บน server ไม่ใช่การ hydrate JavaScript
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”ตอนเจอ snippet ที่ generate มาใช้ server:defer เช็คสิ่งเหล่านี้ก่อนเชื่อ:
server:deferถูกใช้บนหน้า/โปรเจกต์ที่ไม่มี adapter ตั้งไว้ไหม? เป็น build error จริงที่เกิดทันที (NoAdapterInstalledServerIslandsพิสูจน์ข้างบน) ไม่ใช่แค่ warning และไม่ใช่แค่โผล่ตอน runtime- component สมมติว่า props ถูกส่งแบบ plain text บนสายเพราะ “มันก็แค่ query string” ไหม? พิสูจน์ข้างบน: param
e/p/sเข้ารหัสด้วย AES-GCM จริง ไม่ใช่แค่มีอยู่เฉย ๆ — แต่ก็ไม่ได้ฟรีจากต้นทุนด้วย (key ใหม่ทุก build เว้นแต่ตั้งASTRO_KEY) - comment หรือข้อสมมติไหนอ้างว่า response ของ server island “cache อัตโนมัติ” หรือ “cache ที่ edge เหมือนเปลือก” ไหม? พิสูจน์ข้างบนด้วย
curl -D -จริง: ไม่มีCache-Controlheader โดย default เลย — cache ได้แค่เปลือกรอบ ๆ เว้นแต่ component จะตั้ง header เอง server:deferถูกเอามาใช้กับ component ที่ต้องตอบสนอง click ไหม? นั่นคือ client interactivity — เป็น client directive ไม่ใช่ server island ทั้งสองแก้ปัญหาคนละแบบ ใช้ร่วมกันได้แต่แทนกันไม่ได้- fallback (
slot="fallback") render อะไรที่แอบสมมติ cookie/สถานะ auth ไหม (ซึ่งขัดกับจุดประสงค์ของเปลือกที่เร็วและ cache ได้)? fallback คือสิ่งที่ทุกคนเห็นก่อน รวมถึงตอน cache ด้วย มันควรจะกลางๆ โดยดีไซน์ ส่วนที่ personalize มาถึงแค่ตอน fetch ที่ defer เท่านั้น
🐛 หา bug ในโค้ดนี้
---// src/pages/dashboard.astro — a project with output: 'static' and no adapter configuredimport Avatar from '../components/Avatar.astro';---<Avatar server:defer />เฉลย
นี่คือการใช้ server:defer ที่ syntax ถูกต้องสมบูรณ์ — bug ไม่ได้อยู่ในไฟล์นี้เลย มันอยู่ใน astro.config.mjs ของโปรเจกต์: output: 'static' โดยไม่มี adapter เลย server:defer render บน server เสมอ บน request ของตัวเอง ซึ่งต้องการความสามารถ render แบบ on-demand — build จึงพังทันทีและชัดเจน ทำซ้ำจริงข้างบนแล้ว: [NoAdapterInstalledServerIslands] Cannot use server islands without an adapter. ทางแก้ไม่ได้อยู่ใน component นี้ แต่คือเพิ่ม adapter อะไรก็ได้ (adapter: node({ mode: 'standalone' }), Cloudflare, Vercel…) เข้าไปใน config แม้ว่าหน้าอื่นทุกหน้าบนไซต์อาจ static เต็มตัวและไม่ต้องการ adapter เลยก็ตาม
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”server island คือภาพสะท้อนกลับของ client island: client island ส่ง JavaScript เพื่อ defer ตอนที่ component กลายเป็น interactive; server island defer ตอนที่ server render ของ component เกิดขึ้น — หลังจากเปลือกส่งออกไปแล้ว บน request ของตัวเอง เพื่อไม่ให้ส่วนที่ช้าหรือ personalize บล็อกส่วนที่เร็วและ cache ได้ ทั้งคู่คือ “defer ชิ้นนี้ไว้ ส่งที่เหลือทั้งหมดตอนนี้” — อันหนึ่งอยู่บน timeline ของ client อีกอันอยู่บน timeline ของ server
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| หน้า prerendered | หน้า on-demand | server island (server:defer) |
|
|---|---|---|---|
| cache ได้ไหม | ได้เต็มที่ | ไม่ได้โดย default | เปลือกได้ ส่วน island ได้เฉพาะถ้าตั้ง Cache-Control เอง (พิสูจน์ข้างบน) |
| เห็น cookie/session ไหม | ไม่ (build ครั้งเดียว ก่อนมี request ไหนเลย) | ได้ | ได้ — พิสูจน์ข้างบนแล้วว่า cookie จริงไปถึง Astro.request.headers |
| ต้องมี adapter ไหม | ไม่ต้อง | ต้อง | ต้อง — พิสูจน์ข้างบนแล้ว แม้ไซต์ที่เหลือจะ static ทั้งหมด |
| บล็อก first paint ตรงส่วนที่ช้าไหม | N/A (ไม่มีอะไร dynamic) | ใช่ ทั้งหน้ารอ | ไม่ — เปลือกออกก่อน island มาทีหลัง สลับเข้าไปทับ fallback |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “response ของ server island เอง cache พร้อมเปลือกด้วย” พิสูจน์ข้างบนด้วย
curl -D -จริง: ไม่มีCache-Controlheader เลยโดย default การ cache fragment เป็นสิ่งที่ต้อง opt-in เอง ข้างในตัว component - “props ที่ส่งให้ server island มองเห็นได้ใน page source เหมือน query string ธรรมดา” พิสูจน์ข้างบนและยืนยันในซอร์สของ Astro เอง: มันเข้ารหัสด้วย AES-GCM ไม่ใช่ plain text — แม้ key จะใหม่ทุก build เว้นแต่ตั้ง
ASTRO_KEYซึ่งสำคัญสำหรับ CDN และ rolling deploy - “ไซต์ที่ static ทั้งหมดไม่ต้องการ adapter เลย” จริงจนกว่าจะมี
server:deferตัวแรก — พิสูจน์ข้างบนแล้วว่า build พังทันที (NoAdapterInstalledServerIslands) ทันทีที่มีสักตัวปรากฏในโปรเจกต์ ไม่ว่าที่เหลือจะ static แค่ไหน - “server island เห็น cookie ของผู้เยี่ยมชมไม่ได้เพราะ fetch เป็น request แยกนอกรอบ” พิสูจน์ข้างบนด้วย browser session จริง: fetch เป็น same-origin ทำให้ browser ส่ง cookie ให้โดย default —
Astro.request.headers.get('cookie')เห็นเหมือนกับที่หน้าโหลดปกติเห็นทุกอย่าง