Next.js 16, Cache Components และ React Compiler: สิ่งที่เปลี่ยนแปลงไปจริงๆ
· 3 min read · Filippo Spinella · Next.js, React, Frontend, Performance
หลายปีที่ผ่านมาหนึ่งในคำถามที่น่ารำคาญที่สุดใน Next.js คือ: "หน้านี้เป็นแบบคงที่หรือไดนามิก" ดูเหมือนเป็นคำถามง่ายๆ จนกว่าคุณจะเพิ่มการเรียก cookies(), fetch ที่มีตัวเลือกต่างๆ, ไคลเอนต์ฐานข้อมูล, CMS, ตะกร้าสินค้า หรือเนื้อหาที่กำหนดเอง
Next.js 16 น่าสนใจเพราะพยายามทำให้การสนทนานี้ลึกลับน้อยลง มันไม่ได้ขจัดความซับซ้อน แต่มันเปลี่ยนโมเดลทางจิต: เส้นทางเป็นไดนามิกตามค่าเริ่มต้น แคชจะประกาศตัวเองเมื่อจำเป็น และ Suspense กลายเป็นวิธีธรรมชาติในการเขียนเชลล์ที่รวดเร็วด้วยชิ้นส่วนที่ยังคงความสดใหม่
คุณลักษณะที่ต้องทำความเข้าใจคือส่วนประกอบแคช Turbopack ที่เสถียร, React Compiler, proxy.ts และ API ที่ทำให้ใช้งานไม่ได้ใหม่มีความสำคัญ แต่สิ่งเหล่านี้เกี่ยวข้องกับปัญหาเดียวกัน นั่นคือ การสร้างแอปที่รวดเร็วโดยไม่ต้องเดาว่าเฟรมเวิร์กตัดสินใจอะไรอยู่เบื้องหลัง
เพราะสิ่งนี้สำคัญ
ในแอปจริง คุณไม่ได้มีแค่ "หน้าคงที่" และ "หน้าไดนามิก" เท่านั้น คุณมีชิ้นส่วนที่แตกต่างกันและมีความต้องการที่แตกต่างกัน
แผ่นผลิตภัณฑ์อาจมีการเปลี่ยนแปลงสองครั้งต่อวัน ราคาอาจมีการเปลี่ยนแปลงบ่อยขึ้น ความพร้อมใช้งานจะต้องเกือบจะใช้งานได้จริง ชื่อผู้ใช้เป็นเรื่องส่วนตัว สามารถสตรีมรีวิวได้ แถบด้านข้างสามารถมีเสถียรภาพได้ รถเข็นไม่ได้.
หากคุณปฏิบัติต่อทุกสิ่งทุกอย่างเป็นหนึ่งเดียว คุณจะจบลงด้วยหนึ่งในสองภาวะสุดโต่งเสมอ:
- การแคชเชิงรุกและความเสี่ยงในการดูข้อมูลเก่า
- การเรนเดอร์แบบไดนามิกทุกที่และประสิทธิภาพแย่เกินความจำเป็น
ส่วนประกอบแคชทำหน้าที่อย่างแม่นยำเพื่อหลีกเลี่ยงตัวเลือกที่ผิดพลาดนี้
แบบอย่างในทางปฏิบัติ
ด้วย cacheComponents: true คุณสามารถประกาศสิ่งที่แคชได้โดยใช้ "use cache" จากนั้น คุณสามารถเชื่อมโยงระยะเวลาและแท็กกับ cacheLife() และ cacheTag() ได้ ส่วนไดนามิกจะยังคงไดนามิกและสามารถแยกได้ด้วย Suspense
การตั้งค่ามีขนาดเล็ก:
// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { cacheComponents: true, }; export default nextConfig;
การเปลี่ยนแปลงครั้งใหญ่ไม่ได้อยู่ในการกำหนดค่า มันอยู่ที่คุณเริ่มเขียนส่วนประกอบอย่างไร
// app/products/[slug]/page.tsx import { Suspense } from 'react'; import { cacheLife, cacheTag } from 'next/cache'; async function getProduct(slug: string) { 'use cache'; cacheLife('hours'); cacheTag(`product:${slug}`); return db.product.findUnique({ where: { slug } }); } async function ProductDetails({ slug }: { slug: string }) { const product = await getProduct(slug); return ( <section> <h1>{product.name}</h1> <p>{product.description}</p> </section> ); } async function LiveInventory({ slug }: { slug: string }) { const inventory = await db.inventory.findFirst({ where: { slug } }); return <p>{inventory.quantity} pezzi disponibili</p>; } export default async function ProductPage({ params }: { params: Promise<{ slug: string }> }) { const { slug } = await params; return ( <> <ProductDetails slug={slug} /> <Suspense fallback={<p>Controllo disponibilità...</p>}> <LiveInventory slug={slug} /> </Suspense> </> ); }
หน้านี้ไม่จำเป็นต้องแคชทั้งหมดหรือไดนามิกทั้งหมด แผ่นผลิตภัณฑ์สามารถใช้งานได้รวดเร็วและนำกลับมาใช้ใหม่ได้ สินค้าคงคลังสามารถคงความสดได้ ผู้ใช้มองเห็นบางสิ่งบางอย่างได้ทันทีโดยไม่ต้องรอส่วนที่ช้าที่สุด
use cache เป็นเอกสารปฏิบัติการ
สิ่งที่ฉันชอบเกี่ยวกับ "use cache" ก็คือมันบังคับให้คุณแสดงเจตนาอย่างชัดเจน เมื่อคุณอ่านฟังก์ชัน คุณจะเข้าใจทันทีว่ามีคนตัดสินใจว่า "ข้อมูลนี้สามารถนำมาใช้ซ้ำได้"
ซึ่งจะมีประโยชน์อย่างยิ่งเมื่อคุณไม่ได้ใช้ fetch แอพจำนวนมากอ่านข้อมูลจาก Prisma, Drizzle, SDK ภายใน, ไคลเอนต์ CMS หรือฟังก์ชั่นบริการ ในกรณีเหล่านั้น การใช้เหตุผลแบบเก่าตามตัวเลือก fetch เท่านั้นยังไม่เพียงพอ
กฎง่ายๆ:
- เนื้อหาแคชค่อนข้างเสถียร
- ใช้แท็กแบบละเอียด
- ออกจากสิทธิ์แบบไดนามิก เซสชัน รถเข็น การแจ้งเตือน และสถานะธุรกรรม
- ใส่ส่วนที่ช้าเข้าไปข้างใน
Suspense; - วัดผลก่อนที่จะพูดว่า "เราปรับปรุงประสิทธิภาพ"
ทำให้เป็นโมฆะโดยไม่ต้องทิ้งทุกสิ่งทุกอย่าง
แคชจะมีประโยชน์ก็ต่อเมื่อคุณสามารถอัปเดตได้อย่างถูกต้อง ที่นี่ cacheTag, revalidateTag และ updateTag มีความสำคัญ
ตัวอย่าง:
'use server'; import { updateTag } from 'next/cache'; export async function updateProductName(productId: string, name: string) { await db.product.update({ where: { id: productId }, data: { name }, }); updateTag(`product:${productId}`); }
รายละเอียดที่สำคัญคือแท็ก product:${productId} บอกขอบเขตที่ชัดเจน products บอกถังขนาดใหญ่ ในตอนแรกถังขนาดใหญ่ก็สบาย หลังจากผ่านไปไม่กี่เดือน เหตุผลก็คือคุณทำให้แอปครึ่งหนึ่งไม่สามารถเปลี่ยนชื่อได้
Stable Turbopack: ส่วนที่คุณได้ยินทุกวัน
Next.js 16 นำ Turbopack มาสู่ศูนย์กลางสำหรับการพัฒนาและการสร้าง มันไม่ใช่ฟีเจอร์ที่ไพเราะที่สุด แต่เป็นฟีเจอร์ที่คุณรู้สึกได้ในขณะทำงาน: เซิร์ฟเวอร์ที่เริ่มทำงานเร็วขึ้น รีเฟรชเร็วขึ้น สร้างที่หยุดรู้สึกเหมือนต้องพักดื่มกาแฟ
อย่างที่กล่าวไปแล้ว ฉันจะไม่ย้ายฐานโค้ดที่เต็มไปด้วยปลั๊กอินแบบกำหนดเองโดยที่หลับตา ฉันจะตรวจสอบ:
- งานสร้างในท้องถิ่น
- การนำเข้าที่ไม่ได้มาตรฐาน
- MDX, SVG และ CSS;
- เหลือปลั๊กอิน Webpack;
- หน้าที่สำคัญ
- ความแตกต่างในเวลาสร้าง
สำหรับโปรเจ็กต์ใหม่ ฉันจะเริ่มจากค่าเริ่มต้น สำหรับผู้ใหญ่ ฉันจะทำการโยกย้ายแบบวัดผล
React Compiler: ลบเสียงรบกวน ไม่ใช่ความคิด
React Compiler 1.0 มีความเสถียร และ Next.js 16 รองรับด้วย reactCompiler คำมั่นสัญญาคือจะลดการจดจำด้วยตนเองให้น้อยลง: memo น้อยลง, useMemo น้อยลง, useCallback น้อยลง ใช้ "เพื่อความปลอดภัย"
// next.config.ts import type { NextConfig } from 'next'; const nextConfig: NextConfig = { reactCompiler: true, }; export default nextConfig;
ฉันจะไม่ปฏิบัติต่อมันเหมือนฝุ่นวิเศษ คอมไพเลอร์จะช่วยเมื่อโค้ดเป็นไปตามกฎ React อย่างดี หากส่วนประกอบมีผลข้างเคียงแปลกๆ การกลายพันธุ์ที่ซ่อนอยู่ หรือมีการใช้งานที่ไม่ดี จะต้องได้รับการแก้ไขก่อน
วิธีที่ดีต่อสุขภาพที่ควรลอง:
- อัปเดต
eslint-plugin-react-hooks; - แก้ไขการละเมิดที่เกิดขึ้นจริง
- เปิดใช้งานบนพื้นที่ควบคุม
- วัดเวลาและพฤติกรรมในการสร้าง
- ลบการจดจำด้วยตนเองเฉพาะเมื่อไม่ต้องการอีกต่อไป
เป้าหมายไม่ใช่การลบทุกๆ useMemo เป้าหมายคือการหยุดเขียนบันทึกเชิงป้องกันเพราะเรากลัวการแสดงผล
proxy.ts และขอบเขตเครือข่าย
middleware.ts แบบเก่าจะกลายเป็น proxy.ts เป็นการเปลี่ยนชื่อ แต่ก็สมเหตุสมผลแล้ว: ไฟล์นั้นอยู่ที่ขอบเขตคำขอ ไม่ใช่มิดเดิลแวร์ทั่วไปแบบแบ็กเอนด์แบบดั้งเดิม
// proxy.ts import { NextRequest, NextResponse } from 'next/server'; export default function proxy(request: NextRequest) { const isLoggedIn = Boolean(request.cookies.get('session')); if (!isLoggedIn && request.nextUrl.pathname.startsWith('/dashboard')) { return NextResponse.redirect(new URL('/login', request.url)); } return NextResponse.next(); }
กฎง่ายๆ ที่นี่: ทำให้มันเล็ก การเปลี่ยนเส้นทาง การกำหนดเส้นทางการรับรองความถูกต้อง ส่วนหัว การเขียนใหม่ที่จำเป็น ถ้ามันเริ่มรู้สึกเหมือนเป็นแบ็กเอนด์ที่สอง มันอาจจะทำมากเกินไป
ฉันจะย้ายถิ่นฐานได้อย่างไร
ฉันจะไม่เปิดคุณสมบัติทั้งหมดพร้อมกัน ฉันจะทำสิ่งนี้:
- ฉันอัปเดต Next, React และ React DOM;
- ฉันเปิดตัว codemod อย่างเป็นทางการ
- ฉันแก้ไขการเปลี่ยนแปลงที่เสียหายใน
params,searchParams,cookies(),headers()และdraftMode(); - ฉันย้าย
middleware.tsไปที่proxy.ts; - ฉันตรวจสอบบิลด์และเพจที่สำคัญ
- ฉันเปิดใช้งานส่วนประกอบแคชในส่วนที่แคชสร้างความขัดแย้งในปัจจุบัน
- ฉันกำหนดแบบแผนสำหรับแท็กและการทำให้ใช้ไม่ได้
- ฉันลองใช้ React Compiler แยกกัน
- การเปรียบเทียบตัวชี้วัดก่อนและหลัง
การโยกย้ายที่ดีไม่ใช่การโยกย้ายที่ใช้คุณสมบัติใหม่ทั้งหมด เป็นสิ่งที่ทำให้พฤติกรรมของแอปอ่านง่ายขึ้น
สิ่งที่เปลี่ยนแปลงไปในวิธีคิด
สิ่งที่มีประโยชน์ที่สุดเกี่ยวกับ Next.js 16 คือมันบังคับให้คุณตั้งชื่อความตั้งใจได้ดีขึ้น ฟังก์ชันไม่ใช่แค่ "รับผลิตภัณฑ์จากฐานข้อมูล" เท่านั้น มันขึ้นว่า "รับสินค้า ฉันสามารถแคชได้เป็นชั่วโมง ฉันทำให้สินค้าใช้งานไม่ได้ด้วยแท็กนี้" ส่วนประกอบไม่ได้เป็นเพียง "เรนเดอร์เพจ" เท่านั้น มันคือ "นี่คือสิ่งที่รวดเร็ว งานชิ้นนี้เป็นเรื่องส่วนตัว และมาพร้อมกับการสตรีม"
ตอนแรกเหมือนจะมีงานมากขึ้น แล้วมันจะกลายเป็นความสงบ การตัดสินใจด้านประสิทธิภาพจะไม่ถูกซ่อนอยู่ในการผสมผสานระหว่างค่าเริ่มต้น การวิเคราะห์พฤติกรรม และความทรงจำของชนเผ่าอีกต่อไป พวกเขาอยู่ในรหัส