Loading Data
data มาจาก load function
หัวข้อที่มีชื่อว่า “data มาจาก load function”เพจไม่ได้ fetch ข้อมูลของตัวเองใน component แต่ load function ที่อยู่ข้าง ๆ เพจจะ return ข้อมูล แล้ว SvelteKit ส่งให้เพจเป็น data prop เพราะ load รันก่อนเพจ render (บน server สำหรับ request แรก) ข้อมูลจึงอยู่ใน HTML ตั้งแต่แรก — ไม่มีอาการกระพริบตอนโหลด และดีต่อ SEO
export async function load({ params, fetch }) { const res = await fetch(`/api/posts/${params.slug}`); return { post: await res.json() }; // becomes the `data` prop}<script> let { data } = $props(); // data.post is here</script>
<h1>{data.post.title}</h1>flowchart LR
req["request /blog/hello"] --> load["load({ params, fetch })"]
load --> data["return { post }"]
data --> page["+page.svelte ได้ data prop"]
page --> html["rendered HTML"] universal vs server-only load
หัวข้อที่มีชื่อว่า “universal vs server-only load”load มีสองแบบ และการเลือกให้ถูกคือการตัดสินใจสำคัญ:
+page.js— universal load รันบน server สำหรับ render แรก แล้วรันใน browser สำหรับ client-side navigation ครั้งต่อ ๆ ไป ใช้กับข้อมูลที่ fetch ที่ไหนก็ปลอดภัย (public API ผ่านfetchที่ได้มาให้)+page.server.js— server-only load รันบน server เสมอ ไม่เคยรันใน browser ใช้เมื่อโค้ดต้องอยู่ฝั่ง server: query database ตรง ๆ, secret, private environment variable, อ่าน cookie สำหรับ auth ข้อมูลที่ return ต้อง serializable (เพราะถูกส่งไป client)
// src/routes/dashboard/+page.server.js — server-onlyimport { db } from '$lib/server/db';
export async function load({ locals }) { // locals.user was set by a hook; db + secrets never reach the browser. return { projects: await db.projectsFor(locals.user.id) };}กฎง่าย ๆ: ถ้าแตะ database, secret หรือ private env ให้ไว้ใน +page.server.js นอกนั้น +page.js ก็พอ และยังให้ client navigation refetch ได้โดยไม่ต้องวิ่งไป server เทียบกันเต็ม ๆ:
+page.js (universal) | +page.server.js (server-only) | |
|---|---|---|
| รันที่ไหน | server (render แรก) แล้ว browser (client nav) | server เสมอ — ไม่เคยส่งไป browser |
| แตะ database / secret / private env ได้ไหม | ไม่ได้ — โค้ดนั้นจะถูกส่งไป browser | ได้ |
fetch | fetch พิเศษของ SvelteKit (จัดการ relative URL, forward cookie ตอน SSR) | fetch พิเศษตัวเดียวกัน บวกเรียก server ตรง ๆ ได้ |
cookies, locals | ไม่มี | มี |
| ค่าที่ return | ต้อง serializable หรือใช้ตรง ๆ ใน component tree ได้เลย | ต้อง serializable (เข้ารหัสด้วย devalue ไปที่ client) |
ใช้กับ +layout ด้วย | +layout.js | +layout.server.js |
load event, depends() และ invalidation
หัวข้อที่มีชื่อว่า “load event, depends() และ invalidation”load function รับ event object ที่มี field ที่มีประโยชน์: params (route params), fetch, url (URL ที่ร้องขอ), setHeaders, cookies (server load เท่านั้น), locals (server load เท่านั้น), parent (เพื่อ await ข้อมูลจาก parent layout) และ depends (เพื่อลงทะเบียน invalidation key ของตัวเอง)
เรียก depends('app:posts') ใน load จะผูกการรันซ้ำของ load นั้นกับ key นั้น ทีหลังเรียก invalidate('app:posts') (จาก $app/navigation) จะรัน load ทุกตัวที่เรียก depends ด้วย key นั้นซ้ำ — โดยไม่ reload ทั้งหน้า invalidateAll() จะรัน load function ทุกตัว ของเพจปัจจุบัน ไม่สนใจ key เลย
load ใน +layout.js / +layout.server.js ให้ข้อมูลกับ layout และ ทุก child page — เหมาะกับสิ่งที่ทุกเพจต้องใช้ (current user, site settings) load ของ child page อ่านข้อมูลนั้นได้ผ่าน parent() แล้วค่าที่ child return จะถูก merge ทับข้างบนสำหรับ data prop
export function load({ depends }) { depends('app:site'); // this layout's data can be re-run on demand return { site: { name: 'Acme' } };}export async function load({ parent, depends }) { depends('app:settings'); const { site } = await parent(); // merge in the parent layout's data return { site, theme: 'dark' }; // data prop = { site, theme }}พิสูจน์ depends และ parent() โดยเรียก load ทั้งสองตัวตรง ๆ ด้วย fake event แบบเดียวกับที่ unit-test มัน (ไม่ต้องใช้ browser):
import { test, expect } from 'vitest';import { load as layoutLoad } from './+layout.server.js';import { load as pageLoad } from './+page.server.js';
test('+layout.server.js registers a depends key and returns site data', () => { const deps: string[] = []; const data = layoutLoad({ depends: (k: string) => deps.push(k) } as any); expect(deps).toEqual(['app:site']); expect(data).toEqual({ site: { name: 'Acme' } });});
test('+page.server.js merges the parent layout data via parent()', async () => { const deps: string[] = []; const data = await pageLoad({ depends: (k: string) => deps.push(k), parent: async () => ({ site: { name: 'Acme' } }), } as any); expect(deps).toEqual(['app:settings']); expect(data).toEqual({ site: { name: 'Acme' }, theme: 'dark' });});✓ +layout.server.js registers a depends key and returns site data✓ +page.server.js merges the parent layout data via parent()จาก component คุณจะเรียก invalidate('app:settings') หลัง action บางอย่างเพื่อให้ load ตัวนั้นตัวเดียวรันซ้ำ หรือ invalidateAll() เพื่อ refresh ข้อมูลทั้งเพจ:
<script> import { invalidate, invalidateAll } from '$app/navigation';</script>
<button onclick={() => invalidate('app:settings')}>Refresh settings only</button><button onclick={() => invalidateAll()}>Refresh everything</button>Streaming: return promise ที่ไม่ await
หัวข้อที่มีชื่อว่า “Streaming: return promise ที่ไม่ await”ตัวอย่างข้างบนทุกอันจะ await ข้อมูลก่อน return เสมอ — ทั้งเพจจึงรอส่วนที่ช้าที่สุด SvelteKit ทำ streaming แทนได้: return promise โดย ไม่ await แล้วใช้ {#await} ใน template เพื่อรับมัน HTML แรกจะถูกส่งทันทีที่ส่วนที่ await แล้วพร้อม ส่วนที่ stream จะเติมเข้ามาทีหลังเมื่อ promise ของมัน resolve — time-to-first-byte ต่ำลงสำหรับส่วนที่สำคัญ โดยไม่ต้องรอส่วนที่ไม่สำคัญ
/** @param {string} slug */function loadComment(slug) { return new Promise((resolve) => { setTimeout(() => resolve(`a slow comment on ${slug}, arrived late`), 2000); });}
export function load({ params }) { return { comment: loadComment(params.slug), // NOT awaited — a streamed promise };}export async function load({ params, fetch, data }) { const res = await fetch(`/api/posts/${params.slug}`); return { post: await res.json(), comment: data.comment, // forward the server load's streamed promise unchanged };}<script> let { data } = $props(); // data.post and data.comment, merged from both load files</script>
<h1>{data.post.title}</h1>
{#await data.comment} <p>Loading comment…</p>{:then comment} <p>{comment}</p>{/await}พิสูจน์: สคริปต์ Node อ่าน response เป็น stream แล้วจับเวลาแต่ละ chunk เทียบกับ setTimeout 2 วินาทีใน loadComment
$ node stream-timing.mjst=36ms first bytes arrivedt=36ms saw "loading comment…" in the stream (pending state)t=2034ms saw resolved comment text in the streamt=2035ms stream endedแบบเดียวกันด้วย curl --no-buffer ธรรมดา เช็คว่ามี byte มาถึงกี่ byte แล้วหลังจากผ่านไปแค่ 300ms:
$ ( time curl --no-buffer -s http://localhost:5196/blog/hello-world -o /tmp/out.html ) &$ sleep 0.3 && wc -c /tmp/out.html && grep -o "loading comment" /tmp/out.html 3825 /tmp/out.htmlloading comment$ waitcurl --no-buffer -s http://localhost:5196/blog/hello-world -o 0.01s user 0.01s system 0% cpu 2.019 total$ grep -o "a slow comment, arrived late" /tmp/out.htmla slow comment, arrived lateที่ 300ms มี 3825 byte — รวม markup สถานะ pending loading comment… — มาถึงแล้ว ขณะที่ request ทั้งก้อนจบที่ 2.019s ตอน promise ของ comment resolve เท่านั้น ตัว shell ของเพจไม่ได้รอส่วนที่ช้าเลย
$app/state เทียบกับ $app/stores ที่เลิกใช้แล้ว
หัวข้อที่มีชื่อว่า “$app/state เทียบกับ $app/stores ที่เลิกใช้แล้ว”คอร์สนี้ใช้ page, navigating, และ updated จาก $app/state ทุกที่ (เห็นแล้วในบท routing ตรง page.error) module นั้นต้องการ Svelte 5 โค้ดเก่า — และโค้ดที่ AI generate จำนวนมากที่เทรนจากตัวอย่างก่อน 2.12 — import สิ่งเดียวกันจาก $app/stores แทน ซึ่งเป็น store ที่ต้องอ่านด้วย prefix $
<!-- legacy: $app/stores (deprecated) --><script> import { page } from '$app/stores';</script><h1>{$page.url.pathname}</h1><!-- current: $app/state (Svelte 5) --><script> import { page } from '$app/state';</script><h1>{page.url.pathname}</h1>$app/stores ยังทำงานอยู่ — ยังไม่ถูกลบ — แต่ source ของ SvelteKit ที่ติดตั้งอยู่บอกอนาคตของมันไว้ตรง ๆ ที่ export เลย:
$ grep -n "deprecated" node_modules/@sveltejs/kit/src/runtime/app/stores.js9: * @deprecated Use `$app/state` instead (requires Svelte 5, see docs for more info)33: * @deprecated Use `page` from `$app/state` instead (requires Svelte 5, see docs for more info)50: * @deprecated Use `navigating` from `$app/state` instead (requires Svelte 5, see docs for more info)65: * @deprecated Use `updated` from `$app/state` instead (requires Svelte 5, see docs for more info)migration guide บอกไว้ตรง ๆ: “$app/stores is deprecated and subject to be removed in SvelteKit 3” เวลาอ่านโค้ดที่ generate มาแล้วเจอ import $page จาก $app/stores ให้จำไว้ว่านั่นคือ pattern เก่า ไม่ใช่ฟีเจอร์คนละตัว
อ่านโค้ดที่ AI เขียน
หัวข้อที่มีชื่อว่า “อ่านโค้ดที่ AI เขียน”เวลารีวิวโค้ด load ที่ generate มา เช็คสิ่งเหล่านี้:
awaitค่าที่เพจไม่ต้องใช้ในการ render ครั้งแรก (comment thread, “related posts”) แทนที่จะ return promise แบบไม่ await ให้{#await}stream หรือเปล่า?- import
page/navigating/updatedจาก$app/stores(เลิกใช้ตั้งแต่ Kit 2.12 พิสูจน์แล้วข้างบน) แทน$app/stateหรือเปล่า? - server
loadreturn สิ่งที่ไม่ serializable (class instance, function) แทนข้อมูลธรรมดาหรือเปล่า? loadของเพจเอง fetch ข้อมูลที่loadของ parent layout fetch ไปแล้วซ้ำ แทนที่จะอ่านผ่านparent()หรือเปล่า?depends()ถูกเรียกด้วย key ที่ไม่มีใครเรียกinvalidate()ด้วยเลยหรือเปล่า — เป็นสาย wiring ที่ตายแล้วแต่หน้าตาเหมือนเปิด selective refresh?
🐛 หา bug ในโค้ดนี้
<script>import { page } from '$app/stores';
let pathname = $derived($page.url.pathname);</script>
<h1>{pathname}</h1>เฉลย
โค้ดนี้ compile และรันได้วันนี้จริง — $app/stores ยังไม่ถูกลบ แต่มันคือ module ที่เลิกใช้แล้ว: source ของ SvelteKit เองใส่ @deprecated Use \page` from `$app/state` insteadไว้ทุก export และ migration guide บอกว่า$app/stores"is deprecated and subject to be removed in SvelteKit 3" ทางแก้คือ importpageจาก$app/stateแทน แล้วตัด prefix$ออกทั้งหมด —pageจาก$app/stateเป็น reactive object อยู่แล้ว (backed ด้วย$state) ไม่ใช่ store จึง page.url.pathname` คือทั้งหมด ไม่ต้องใช้ syntax subscription
Mental Model
หัวข้อที่มีชื่อว่า “Mental Model”load function คือ data contract ของ route ไม่ใช่แค่ fetch call ที่แปะไว้ใน component ค่าที่ return จะกลายเป็น data prop ที่ merge มาจากทั้งสาย: +layout.server.js → +layout.js (ผ่าน parent()) → +page.server.js → +page.js ข้อมูลชิ้นไหนจะ block การ render แรกหรือไม่ เป็นตัวเลือกของคุณต่อ field: await มันเพื่อการันตีว่าอยู่ใน HTML แรก หรือ return promise เปล่า ๆ ให้ {#await} stream เข้ามาทีหลัง depends() กับ invalidate() ก็เป็นแนวคิดเดียวกันแต่ใช้กับการรันซ้ำ: load ไม่รู้เองว่าข้อมูลของมัน stale เมื่อไร มันจึงประกาศ key ที่มันขึ้นกับ แล้วให้ที่อื่นตัดสินใจว่าจะ invalidate key นั้นเมื่อไร
Trade-offs
หัวข้อที่มีชื่อว่า “Trade-offs”| ได้อะไร | ประโยชน์ | ต้นทุน |
|---|---|---|
+page.js (universal) | รันใน browser ด้วย — client nav refetch โดยไม่ต้องวิ่งไป server | แตะ secret, database, private env ไม่ได้ |
+page.server.js (server-only) | เข้าถึง server เต็มที่ (db, secret, cookie) | client navigation ทุกครั้งยังต้องวิ่งไป server |
| stream promise ที่ไม่ await | time-to-first-byte ต่ำลงสำหรับส่วนที่สำคัญ | ผู้อ่านเห็น loading state; error ข้างในต้องจัดการใน {#await} (หรือ <svelte:boundary>) แทนที่จะทำให้ทั้งเพจล้ม |
depends() + invalidate() | รันซ้ำแบบเลือกได้ แทนที่จะ invalidateAll() ทั้งหมด | key ที่ไม่มีใคร invalidate เลยคือโค้ดที่ตายแล้วแต่หน้าตาเหมือนทำอะไรบางอย่าง |
Common Misconceptions
หัวข้อที่มีชื่อว่า “Common Misconceptions”- “universal
loadก็แค่ server load ที่ด้อยกว่า” มันเป็นแบบเดียวที่รันใน browser ด้วย — client-side navigation ข้าม server round-trip ไปเลยสำหรับมัน พิสูจน์แล้วในตารางเทียบข้างบน - “ค่าทุกตัวของ
loadต้อง await ก่อน return เสมอ” พิสูจน์แล้วข้างบน: promise ที่ไม่ await จะ stream — shell render ทันทีแล้ว{#await}เติมส่วนที่เหลือทีหลัง - “
$app/storesกับ$app/stateคือชื่อสองชื่อของสิ่งเดียวกัน”$app/storesเลิกใช้แล้ว (พิสูจน์แล้วข้างบน จาก source ของ Kit เองและ migration guide) —pageของ$app/stateเป็น reactive object ธรรมดา ไม่ใช่ store จึงอ่านโดยไม่ต้องมี$ - “
loadของ child page ต้อง fetch สิ่งที่ layout load ไปแล้วซ้ำ”parent()return ข้อมูลที่ resolve แล้วของ parent ให้แทน