ข้ามไปยังเนื้อหา

Islands Architecture และ MPA Model

model การส่งมอบเริ่มต้นของ React คือ Single-Page Application (SPA): server ส่ง HTML shell ที่เกือบว่างเปล่า browser ดาวน์โหลด JavaScript bundle, React บูต และหน้าเว็บ render ฝั่ง client การนำทางจัดการด้วย client-side router — ไม่มีการโหลดหน้าใหม่เต็มรูปแบบ

model ของ Astro คือ Multi-Page Application (MPA): server render แต่ละหน้าเป็น HTML สมบูรณ์ก่อนที่ browser จะเห็น browser รับ HTML จริงๆ วาดภาพทันที และ — ถ้าคุณไม่บอก Astro เป็นอย่างอื่น — ไม่ดาวน์โหลด JavaScript เลย

ใน React SPA ทั่วไป:

  • browser ดาวน์โหลด main.js (React runtime + โค้ดแอปของคุณ — มักจะ 100–300 kB ในรูปแบบ gzip)
  • React รัน, สร้าง virtual DOM, reconcile, และแนบ event listener
  • แล้วหน้าถึงจะโต้ตอบได้

สำหรับบทความบล็อก, landing page, หรือบทความ docs JavaScript ส่วนใหญ่นั้นสูญเปล่า: เนื้อหาไม่เคยเปลี่ยนหลังจาก render ครั้งแรก

islands architecture ของ Astro กลับหลักการนี้ หน้าเป็น static HTML เมื่อ component ใดต้องการ interactivity — search box, counter, ตะกร้าสินค้า — คุณทำเครื่องหมาย component นั้นเป็น island

React
// React SPA — everything is a component tree
// The entire page re-renders when state changes
function App() {
const [count, setCount] = React.useState(0);
return (
<>
<Header /> {/* re-renders on count change */}
<ArticleBody /> {/* re-renders on count change */}
<Counter
count={count}
onIncrement={() => setCount(c => c + 1)}
/>
</>
);
}
// Bundle: React runtime + Header + ArticleBody + Counter
Astro
---
// Astro MPA — only Counter is an island
import Header from '../components/Header.astro';
import ArticleBody from '../components/ArticleBody.astro';
import Counter from '../components/Counter.tsx'; // React island
---
<html lang="en">
<body>
<Header /> <!-- zero JS, pure HTML -->
<ArticleBody /> <!-- zero JS, pure HTML -->
<Counter client:visible /> <!-- hydrated only when visible -->
</body>
</html>
<!-- Bundle: only Counter's JS — not Header or ArticleBody -->

สังเกต client:visible บน Counter นั่นคือ hydration directive ถ้าไม่มี directive นี้ Counter จะ render เป็น HTML ขณะ build (markup เริ่มต้น) และไม่มีทางโต้ตอบใน browser

Directiveเมื่อใด island จะ hydrate
client:loadทันทีเมื่อโหลดหน้า
client:idleเมื่อ browser ว่าง (requestIdleCallback)
client:visibleเมื่อ component เข้ามาใน viewport
client:media="(max-width:768px)"เมื่อ CSS media query ตรงกัน
client:only="react"ข้าม SSR ทั้งหมด; render เฉพาะใน browser

แต่ละ island โหลดและ hydrate แยกกัน component data-visualisation ขนาดใหญ่ที่ด้านล่างของหน้า (client:visible) ไม่บล็อก header จากการวาดภาพ

ลองนึกภาพหน้าที่มี navigation bar, บทความยาว, sidebar related-posts, และปุ่ม “like” ที่โต้ตอบได้หนึ่งปุ่ม:

  • React SPA: ทั้งสี่ส่วนส่งเป็น JavaScript, บูตพร้อมกัน, hydrate พร้อมกัน
  • Astro MPA กับ island: เฉพาะปุ่ม “like” เท่านั้นที่ส่ง JavaScript ส่วนอีกสามส่วนเป็น HTML ล้วนๆ

ผลในทางปฏิบัติคือ Total Blocking Time ที่ลดลงอย่างมากและ Largest Contentful Paint ที่เร็วขึ้น — metric ที่ขับเคลื่อนทั้ง user experience และ SEO

ข้อแลกเปลี่ยน: เนื่องจากแต่ละ Astro page เป็น HTML document แยกกัน การนำทางระหว่างหน้าจึงเป็นการโหลดหน้าเต็มรูปแบบ (เหมือนคลิก link บนเว็บไซต์แบบดั้งเดิม) ไม่มี client-side router ที่แคชหน้าก่อนหน้าในหน่วยความจำ

สำหรับเว็บไซต์ที่เน้นเนื้อหาส่วนใหญ่นี่ก็ไม่เป็นไร — หน้าโหลดเร็วเพราะเป็น HTML ที่ pre-render แล้ว สำหรับแอปที่ใช้งานหนักซึ่ง navigation state มีความสำคัญ React (หรือ Next.js กับ App Router) ยังคงเหมาะกว่า

Astro รองรับ View Transitions (<ClientRouter />) เพื่อทำ animation ระหว่างหน้าโดยไม่ต้องโหลดใหม่เต็มรูปแบบ — แต่นั่นเป็น enhancement แบบ opt-in ไม่ใช่ค่าเริ่มต้น

Astro
---
const sections = [
  { id: "hero", label: "Hero", color: "#6366f1" },
  { id: "about", label: "About", color: "#0ea5e9" },
  { id: "contact", label: "Contact", color: "#10b981" },
];
---
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <title>Islands demo</title>
    <style>
      body { font-family: sans-serif; margin: 0; }
      .section { padding: 3rem 2rem; }
      .badge {
        display: inline-block;
        font-size: 0.75rem;
        padding: 0.2rem 0.6rem;
        border-radius: 999px;
        background: #f1f5f9;
        color: #475569;
        margin-bottom: 0.5rem;
      }
    </style>
  </head>
  <body>
    {sections.map(s => (
      <div class="section" style={`border-top: 4px solid ${s.color}`}>
        <span class="badge">Static HTML island</span>
        <h2>{s.label} section</h2>
        <p>This renders to HTML at build time. No JS bundle.</p>
      </div>
    ))}
    <div class="section" style="border-top:4px solid #f59e0b">
      <span class="badge">client:visible island would go here</span>
      <h2>Interactive widget</h2>
      <p>Only this section would ship JavaScript — the rest stays pure HTML.</p>
    </div>
  </body>
</html>
`client:visible` ใน Astro ทำอะไร?
ใน islands model ของ Astro สิ่งที่เกิดขึ้นกับ `.astro` component ที่ไม่มี directive `client:*` คืออะไร?
ข้อแลกเปลี่ยน performance หลักของ Astro MPA เมื่อเทียบกับ React SPA คืออะไร?
directive `client:*` ใดที่ hydrate component เฉพาะหลังจาก browser ว่าง?