Part of #234 · PRD: docs/prd/2026-07-26-public-read-latency-and-cache-strategy.md (D4)
This is a decision, not an implementation. It is ready-for-human on the one question that is genuinely not mine to settle.
EN
#238–#240 make /projects sub-second while it stays a dynamic route. What they cannot do is make it CDN-class (/faq serves in 0.07–0.11 s without invoking a function), because HTML is still rendered per request. Getting there requires one of two hard-to-reverse changes, and choosing between them is what this issue is for.
Option A — cacheComponents: true. Next 16 removed experimental.ppr; this is the only opt-in that lets a searchParams page have a prerendered static shell with the dynamic part streamed. It changes caching semantics app-wide, so every page needs auditing, and 'use cache'/'use cache: remote' become meaningful only alongside a cacheHandlers durable store (cacheHandlers.md:24,66).
Option B — per-filter prerendered routes. e.g. /projects/c/[category] with generateStaticParams, keeping /projects prerendered for the unfiltered list, plus redirects so existing ?category=… links keep working. Cannot cover q (free-text search) at all, and category × tag × tech is combinatorial — so it is a partial fix by construction.
Option C — accept dynamic. Sub-second from #238–#240 is enough; spend the effort elsewhere.
The ADR must also settle the surfaces this touches beyond /projects: /blog has the identical searchParams shape and is also uncached (0.65 s measured), and /projects/[slug] reads the DB per request too.
Acceptance criteria
TH
ส่วนหนึ่งของ #234 · PRD: docs/prd/2026-07-26-public-read-latency-and-cache-strategy.md (D4)
นี่คือการตัดสินใจ ไม่ใช่การ implement เป็น ready-for-human ในคำถามข้อเดียวที่ไม่ใช่เรื่องที่ผมควรตัดสิน
#238–#240 ทำให้ /projects ต่ำกว่า 1 วินาทีขณะที่มันยังเป็น dynamic route สิ่งที่มันทำไม่ได้คือทำให้เป็นระดับ CDN (/faq เสิร์ฟใน 0.07–0.11 วิ โดยไม่เรียก function) เพราะ HTML ยัง render ต่อ request การไปถึงจุดนั้นต้องใช้การเปลี่ยนแปลงที่ย้อนยากหนึ่งในสองแบบ และการเลือกคือเหตุที่ issue นี้มีอยู่
ทางเลือก A — cacheComponents: true Next 16 ถอด experimental.ppr ออก นี่เป็นทางเปิดใช้เดียวที่ทำให้หน้าที่อ่าน searchParams มี static shell ที่ prerender พร้อมสตรีมส่วน dynamic มันเปลี่ยน semantics การแคช ทั้งแอป ฉะนั้นทุกหน้าต้องถูกตรวจ และ 'use cache'/'use cache: remote' จะมีความหมายเฉพาะเมื่อมี cacheHandlers ที่เก็บแบบ durable ควบคู่ (cacheHandlers.md:24,66)
ทางเลือก B — route ที่ prerender ต่อตัวกรอง เช่น /projects/c/[category] ด้วย generateStaticParams เก็บ /projects ที่ prerender ไว้สำหรับรายการไม่กรอง บวก redirect เพื่อให้ลิงก์ ?category=… เดิมยังทำงาน มันครอบ q (ค้นหาข้อความอิสระ) ไม่ได้เลย และ category × tag × tech เป็น combinatorial จึงเป็นการแก้บางส่วนโดยโครงสร้าง
ทางเลือก C — ยอมรับว่าเป็น dynamic ต่ำกว่า 1 วินาทีจาก #238–#240 พอแล้ว เอาแรงไปทำอย่างอื่น
ADR ต้องตัดสินผิวสัมผัสอื่นที่เกี่ยวด้วย: /blog มีรูปแบบ searchParams เหมือนกันเป๊ะและไม่ถูกแคชเช่นกัน (วัดได้ 0.65 วิ) และ /projects/[slug] ก็อ่าน DB ต่อ request
เกณฑ์รับงาน
Part of #234 · PRD:
docs/prd/2026-07-26-public-read-latency-and-cache-strategy.md(D4)This is a decision, not an implementation. It is
ready-for-humanon the one question that is genuinely not mine to settle.EN
#238–#240make/projectssub-second while it stays a dynamic route. What they cannot do is make it CDN-class (/faqserves in 0.07–0.11 s without invoking a function), because HTML is still rendered per request. Getting there requires one of two hard-to-reverse changes, and choosing between them is what this issue is for.Option A —
cacheComponents: true. Next 16 removedexperimental.ppr; this is the only opt-in that lets asearchParamspage have a prerendered static shell with the dynamic part streamed. It changes caching semantics app-wide, so every page needs auditing, and'use cache'/'use cache: remote'become meaningful only alongside acacheHandlersdurable store (cacheHandlers.md:24,66).Option B — per-filter prerendered routes. e.g.
/projects/c/[category]withgenerateStaticParams, keeping/projectsprerendered for the unfiltered list, plus redirects so existing?category=…links keep working. Cannot coverq(free-text search) at all, and category × tag × tech is combinatorial — so it is a partial fix by construction.Option C — accept dynamic. Sub-second from #238–#240 is enough; spend the effort elsewhere.
The ADR must also settle the surfaces this touches beyond
/projects:/bloghas the identicalsearchParamsshape and is also uncached (0.65 s measured), and/projects/[slug]reads the DB per request too.Acceptance criteria
/blogand/projects/[slug]are each either covered by the decision or explicitly listed as out of scope with a reasonTH
ส่วนหนึ่งของ #234 · PRD:
docs/prd/2026-07-26-public-read-latency-and-cache-strategy.md(D4)นี่คือการตัดสินใจ ไม่ใช่การ implement เป็น
ready-for-humanในคำถามข้อเดียวที่ไม่ใช่เรื่องที่ผมควรตัดสิน#238–#240ทำให้/projectsต่ำกว่า 1 วินาทีขณะที่มันยังเป็น dynamic route สิ่งที่มันทำไม่ได้คือทำให้เป็นระดับ CDN (/faqเสิร์ฟใน 0.07–0.11 วิ โดยไม่เรียก function) เพราะ HTML ยัง render ต่อ request การไปถึงจุดนั้นต้องใช้การเปลี่ยนแปลงที่ย้อนยากหนึ่งในสองแบบ และการเลือกคือเหตุที่ issue นี้มีอยู่ทางเลือก A —
cacheComponents: trueNext 16 ถอดexperimental.pprออก นี่เป็นทางเปิดใช้เดียวที่ทำให้หน้าที่อ่านsearchParamsมี static shell ที่ prerender พร้อมสตรีมส่วน dynamic มันเปลี่ยน semantics การแคช ทั้งแอป ฉะนั้นทุกหน้าต้องถูกตรวจ และ'use cache'/'use cache: remote'จะมีความหมายเฉพาะเมื่อมีcacheHandlersที่เก็บแบบ durable ควบคู่ (cacheHandlers.md:24,66)ทางเลือก B — route ที่ prerender ต่อตัวกรอง เช่น
/projects/c/[category]ด้วยgenerateStaticParamsเก็บ/projectsที่ prerender ไว้สำหรับรายการไม่กรอง บวก redirect เพื่อให้ลิงก์?category=…เดิมยังทำงาน มันครอบq(ค้นหาข้อความอิสระ) ไม่ได้เลย และ category × tag × tech เป็น combinatorial จึงเป็นการแก้บางส่วนโดยโครงสร้างทางเลือก C — ยอมรับว่าเป็น dynamic ต่ำกว่า 1 วินาทีจาก #238–#240 พอแล้ว เอาแรงไปทำอย่างอื่น
ADR ต้องตัดสินผิวสัมผัสอื่นที่เกี่ยวด้วย:
/blogมีรูปแบบsearchParamsเหมือนกันเป๊ะและไม่ถูกแคชเช่นกัน (วัดได้ 0.65 วิ) และ/projects/[slug]ก็อ่าน DB ต่อ requestเกณฑ์รับงาน
/blogและ/projects/[slug]ถูกครอบด้วยการตัดสินใจ หรือถูกระบุชัดว่าอยู่นอกขอบเขตพร้อมเหตุผล