ทำไมต้อง Astro และ Islands
ปัญหา: ส่งทั้ง framework ไปเพื่อ render ตัวหนังสือ
หัวข้อที่มีชื่อว่า “ปัญหา: ส่งทั้ง framework ไปเพื่อ render ตัวหนังสือ”เว็บส่วนใหญ่คือ content: blog, docs, marketing site, รายการสินค้า e-commerce การสร้างของพวกนี้ด้วย SPA ที่ render ฝั่ง client แปลว่า browser ต้องโหลด JavaScript bundle ก้อนใหญ่ boot framework ขึ้นมา แล้ว re-render — บน client — HTML ที่ทีแรกก็ส่งเป็น HTML ธรรมดาได้อยู่แล้ว
ต้นทุนตรงนี้ไม่ใช่เรื่องเล็ก: first paint ช้าลง, interactivity มาช้า (เข้าสู่ “uncanny valley” ที่หน้าดูเหมือนพร้อมแต่กดอะไรไม่ได้) และเสีย CPU ไปเยอะกับการ hydrate ส่วนที่ไม่เคย interactive เลย — footer, เนื้อบทความ, nav bar
Insight ที่ Astro สร้างขึ้นมา: สำหรับ content site แล้ว HTML คือตัวสินค้า JavaScript ควรเป็นข้อยกเว้น ไม่ใช่ฐานราก
islands architecture
หัวข้อที่มีชื่อว่า “islands architecture”Astro render ทั้งหน้าเป็น static HTML แล้วมองแต่ละ component ที่ interactive เป็น island — ชิ้นเล็ก ๆ ที่ hydrate แยกกันในทะเลของ static HTML
flowchart TB page["Page: static HTML (ไม่มี JS)"] page --> nav["nav — static"] page --> article["เนื้อบทความ — static"] page --> search["SearchBox — island (hydrated)"] page --> cart["CartWidget — island (hydrated)"] page --> footer["footer — static"]
สองคุณสมบัติที่นิยาม island:
- แยกเป็นอิสระ แต่ละ island hydrate ด้วยตัวเอง มี JavaScript ของตัวเอง เป็นอิสระจากตัวอื่น carousel ที่โหลดช้าไม่ block search box
- Opt-in ไม่มีอะไร interactive จนกว่าคุณจะ ทำเครื่องหมาย ไว้เอง (ด้วย client directive — มีบทเรียนทั้งบททีหลัง) ค่า default คือ static HTML ที่ไม่มี JavaScript เลย
เทียบกับ SPA ที่ ทั้งหน้า เป็น JavaScript application ก้อนเดียวซึ่งต้อง boot ให้เสร็จก่อนถึงจะทำอะไรได้ ส่วน islands ส่ง JavaScript เฉพาะ component ที่ต้องใช้ และส่งแค่เท่าที่ต้องใช้
Zero JS by default
หัวข้อที่มีชื่อว่า “Zero JS by default”นี่คือพาดหัว: Astro component โดย default ผลิต HTML กับ CSS และไม่มี JavaScript เขียน component ที่มีปุ่มและ onClick — เว้นแต่คุณจะเปลี่ยน component นั้นเป็น island — ปุ่มจะ render แต่ handler ไม่ถูกส่งไป นั่นไม่ใช่ข้อจำกัด แต่เป็นประเด็นสำคัญ: คุณถูกบังคับให้ตั้งใจว่าอะไรจะรันบน client
---// This component runs on the server to produce HTML.// None of this JavaScript is sent to the browser.const posts = await getPosts();---<ul> {posts.map((post) => <li>{post.title}</li>)}</ul><!-- Ships: an HTML <ul>. Ships: no JavaScript. -->ผลลัพธ์คือหน้าเว็บที่เร็วตั้งแต่โครงสร้าง: browser ได้ HTML ที่ paint ได้ทันที และโหลด JavaScript เฉพาะ island ที่คุณเลือกเปิดเองอย่างชัดเจน
พิสูจน์: build จริง ๆ ส่งอะไรออกไป
หัวข้อที่มีชื่อว่า “พิสูจน์: build จริง ๆ ส่งอะไรออกไป”“Zero JS by default” เป็นข้ออ้างที่เช็คได้ ไม่ใช่แค่สโลแกน — build สองหน้าคู่กันแล้วอ่าน dist/ จริง ๆ อย่างแรก หน้าที่มี island เป็นศูนย์ (list ธรรมดา render ฝั่ง server):
---const posts = [{ title: 'One' }, { title: 'Two' }];---<html><body><ul>{posts.map((p) => <li>{p.title}</li>)}</ul></body></html>อย่างที่สอง shell เดิมแต่มี island หนึ่งตัว — Preact counter ที่ hydrate ด้วย client:visible:
import { useState } from 'preact/hooks';
export default function Counter() { const [n, setN] = useState(0); return <button onClick={() => setN(n + 1)}>Count: {n}</button>;}---import Counter from '../components/Counter.tsx';---<html><body><Counter client:visible /></body></html>astro build จริงของทั้งสองหน้า แล้วดู output tree จริง:
$ npx astro build --outDir dist$ find dist/client -type f | sort_astro/Counter.D0Rq_C0J.js_astro/client.CYoOuZrL.js_astro/hooks.module.C-rKEPwH.js_astro/preact.module.Bl7PEaKa.js_astro/signals.module.r9f66GF9.jsfavicon.icofavicon.svgindex.htmlone-island/index.htmlzero-js/index.html
$ grep -o '<script[^>]*>' dist/client/zero-js/index.html(no output — no <script> tag at all)
$ grep -o '<script[^>]*>' dist/client/one-island/index.html<script><script>zero-js/index.html ไม่ส่ง chunk _astro/*.js แม้แต่ชิ้นเดียว และไม่มี <script> tag เลย — เป็น HTML ล้วน ๆ จบ การเพิ่ม island แค่ตัวเดียว (one-island.astro) ต่างหากที่ดึง Preact runtime, chunk ของ component นั้นเอง และ client hydration bootstrap เข้ามา — JS ห้าไฟล์สำหรับทั้งไซต์ และทุกไฟล์สืบไปหา client:visible ตัวเดียวนั้นได้หมด ลบ island ทิ้ง JS ก็หายไปด้วย ไม่มีอะไรอื่นบนหน้าต้องจ่ายค่านี้
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”โค้ด Astro ที่ AI generate มักพกสมมติฐานจาก tutorial React/Vue มาซึ่งใช้ไม่ได้ที่นี่ เช็คสิ่งเหล่านี้:
- component ใช้
useState,useEffect, หรือ hook ใด ๆ ที่ top level ของ frontmatter.astroหรือเปล่า? hook รันได้เฉพาะใน framework component ที่ hydrate แล้ว (island) เท่านั้น — ใน script ฝั่ง server ของไฟล์.astroมันเป็นได้ทั้ง error ตรง ๆ (import ผิด) หรือโค้ดตายที่ไม่เคยรันฝั่ง client เลย - มี client directive (
client:load,client:visible,client:idle, …) อยู่บน component ทุกตัวที่โค้ดคาดว่าจะ interactive หรือเปล่า? framework component (.tsx/.vue/.svelte) ที่ฝังใน Astro โดย ไม่มี directive จะ render เฉพาะ markup แบบ static — event handler ของมันถูกตัดทิ้งเงียบ ๆ ไม่ใช่แค่ “ยัง hydrate ไม่ทัน” - คำอธิบายอ้างว่า “Astro hydrate ทั้งหน้า” หรือเปล่า? มันไม่ใช่ — มีแค่ component ที่ทำเครื่องหมายเป็น island อย่างชัดเจนเท่านั้นที่ hydrate ส่วนที่เหลือเป็น static HTML ตลอดไป ตามที่พิสูจน์ไว้ข้างบน
- มี
onClick/onSubmitติดอยู่บน element ใน template.astroธรรมดา (ไม่ได้อยู่ใน island) หรือเปล่า? handler แบบนั้นเป็นโค้ดตายฝั่ง server — มันถูกตัดทิ้ง เพราะ template.astroไม่เคยส่ง client JavaScript ของตัวเองออกไป
🐛 หา bug ในโค้ดนี้
// src/components/LikeButton.tsx (a Preact component)import { useState } from 'preact/hooks';
export default function LikeButton() {const [liked, setLiked] = useState(false);return <button onClick={() => setLiked(!liked)}>{liked ? 'Liked' : 'Like'}</button>;}
// --- used with no client directive: ---// src/pages/index.astro// <LikeButton />เฉลย
<LikeButton /> ไม่ได้ตั้งค่า client directive ไว้เลย Astro เลย render มันครั้งเดียวบน server แล้วส่งเฉพาะ HTML แบบ static ที่ได้ <button>Like</button> — hook useState กับ handler onClick ไม่ไปถึง browser เลย กดปุ่มแล้วไม่เกิดอะไรขึ้น เพราะไม่มี JavaScript คอยฟังอยู่ วิธีแก้คือเพิ่ม attribute เดียว: <LikeButton client:visible /> (หรือ client:load/client:idle) ซึ่งบอก Astro ให้ส่ง JS ของ component นี้ไปด้วยและ hydrate มัน — พิสูจน์แล้วข้างบนว่าการ hydrate หน้าที่เดิม static ล้วน ๆ คือสิ่งที่ดึงไฟล์ _astro/*.js ทั้งห้าไฟล์เข้ามา
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”หน้า Astro เริ่มต้นจาก HTML ที่ตัด interactivity ออกไปด้วยมีดผ่าตัด ไม่ใช่ JavaScript app ที่เพิ่ม HTML กลับเข้ามาทีหลัง ทุก component โดย default คือ “print แล้วจบ” คุณต้องพูดชัด ๆ ว่า “ให้ตัวนี้มีชีวิตต่อใน browser” ด้วย client directive การกลับด้าน (opt-in JS แทนที่จะเป็น opt-out) นี่แหละคือกลไกทั้งหมดที่อยู่เบื้องหลังการพิสูจน์ด้วย build ข้างบน
ข้อแลกเปลี่ยน
หัวข้อที่มีชื่อว่า “ข้อแลกเปลี่ยน”| Islands (Astro) | SPA เต็มตัว | |
|---|---|---|
| JS ที่ส่งสำหรับส่วน static | ไม่มี | ทั้ง app bundle แม้แต่ตัว footer |
| ความละเอียดของ interactivity | ต่อ component, opt-in | all-or-nothing, เปิดโดย default |
| Time-to-interactive สำหรับหน้า content | เร็ว — HTML paint ได้ทันที | ช้ากว่า — framework ต้อง boot ก่อน |
| เหมาะกับ | content-driven site ที่มีจุด interactive เป็นหย่อม ๆ | UI แบบแอปที่เกือบทุกอย่างมี state |
| ต้นทุนของหน้าที่ interactive สูงมาก | island ใหญ่เริ่มขัดกับจุดประสงค์ | นี่คือสนามถนัดของ framework แบบนี้ |
ความเข้าใจผิดที่พบบ่อย
หัวข้อที่มีชื่อว่า “ความเข้าใจผิดที่พบบ่อย”- “Astro hydrate ทั้งหน้า แค่ทำอย่างมีประสิทธิภาพ” ไม่จริง — พิสูจน์แล้วข้างบน: หน้าที่ไม่มี island เลยส่งไฟล์
_astro/*.jsเป็นศูนย์และไม่มี<script>tag ไม่มีอะไร hydrate เว้นแต่ component นั้นถูกทำเครื่องหมายเป็น island อย่างชัดเจน - “framework component (
.tsx) interactive อัตโนมัติเพราะมันคือ React/Preact” มัน render เป็น static HTML เหมือน component อื่นเว้นแต่จะมี client directive ติดอยู่ — ดู bug ของLikeButtonข้างบน - “islands architecture แปลว่าทุกหน้ามี JavaScript อยู่บ้าง” หน้าหนึ่งมี island เป็นศูนย์และส่ง JavaScript เป็นศูนย์ได้ ตามที่
zero-js/index.htmlพิสูจน์ไว้ข้างบน — island เป็น opt-in ต่อ component ไม่ใช่ baseline ที่รับประกันไว้
Astro เหมาะ (และไม่เหมาะ) ตอนไหน
หัวข้อที่มีชื่อว่า “Astro เหมาะ (และไม่เหมาะ) ตอนไหน”Astro โดดเด่นกับ content-driven site: blog, documentation, marketing, portfolio, e-commerce, อะไรก็ตามที่ส่วนใหญ่ของหน้าเป็น content และ interactivity อยู่เฉพาะจุด
Astro เหมาะน้อยกว่ากับ UI ที่เหมือนแอปและ interactive สูง — editor สไตล์ figma, trading dashboard, เครื่องมือ collaborate แบบ realtime — ที่เกือบทุกอย่างมี state และ interactive ตรงนั้น SPA framework เต็มตัว (หรือ Astro ที่มี island ใหญ่มาก ซึ่งขัดกับจุดประสงค์) จะเข้าท่ากว่า และ Astro ยังให้คุณฝัง component ของ framework พวกนั้นเป็น island ได้เมื่อจำเป็น
บทถัดไปใน Islands จะพูดถึง directive (client:load, client:visible, client:idle, …) ที่ทำให้ component กลายเป็น island — บทนี้พิสูจน์แค่ ว่า opt-in นี้มีอยู่จริง ส่วนบทนั้นจะพูดถึง ว่าควรเลือก directive ไหน