Kodokon kodokon.com

Suspense, lazy, และ code splitting

เชี่ยวชาญกลไกการพักการเรนเดอร์ (suspension) การโหลดคอมโพเนนต์แบบ lazy และการแบ่งบันเดิล (bundle splitting)

9 นาที · 3 คำถาม

เปิดบทเรียนนี้ใน Kodokon

Suspense อาศัยกลไกที่คนไม่ค่อยรู้จัก: คอมโพเนนต์ที่ยังไม่มีข้อมูลหรือโค้ดของมันจะ พัก (suspend) การเรนเดอร์ของตัวเอง เบื้องหลัง การนำไปใช้งานปัจจุบันจะโยน promise ออกมา ซึ่ง React ดักจับไว้เหมือนกับที่มันดักจับข้อผิดพลาด จากนั้น React จะไต่ขึ้นไปหา ขอบเขต <Suspense> ที่ใกล้ที่สุด แสดง fallback ของมัน แล้ว เล่นการเรนเดอร์ซ้ำ ของซับทรีเมื่อ promise คลี่คลาย (resolve) นี่เป็นรายละเอียดของการนำไปใช้งาน (อย่าโยน promise ด้วยตัวเองเด็ดขาด) แต่มันอธิบายพฤติกรรมที่สังเกตได้ทั้งหมด: ขอบเขตนี้ทำหน้าที่เหมือน catch แบบประกาศ (declarative) สำหรับงานแบบอะซิงโครนัส กรณีการใช้งานเชิงประวัติศาสตร์คือ code splitting ด้วย React.lazy

JSX
import { lazy, Suspense } from 'react';

const Chart = lazy(() => import('./Chart'));

function Dashboard() {
  return (
    <Suspense fallback={<Spinner />}>
      <Chart points={data} />
    </Suspense>
  );
}
โค้ดของ Chart จะถูกดาวน์โหลดเฉพาะตอนที่มันเรนเดอร์ครั้งแรกเท่านั้น

lazy คาดหวังฟังก์ชันที่คืนค่า promise ซึ่งคลี่คลายไปเป็น โมดูลที่มี export เป็น default ตัวการแบ่ง (splitting) เองเป็นงานของบันเดิลเลอร์ (bundler): แต่ละ import() แบบไดนามิกจะกลายเป็นจุดแบ่ง (split point) และสร้าง ชังก์ (chunk) แยกต่างหาก ที่ถูกโหลดตามต้องการ ความละเอียดอ่อนที่พบบ่อยในโค้ดเบสจริง: คอมโพเนนต์ของคุณมักเป็น named export คุณจึงต้องปรับโมดูลในทันที (on the fly)

JAVASCRIPT
const Chart = lazy(() =>
  import('./charts').then((module) => ({
    default: module.Chart,
  }))
);
การปรับ named export ให้เข้ากับสัญญาของ React.lazy

ส่วนที่ยุ่งยากในโปรดักชัน: ระหว่าง การนำทาง (navigation) เนื้อหาที่แสดงอยู่แล้วสามารถพักได้อีกครั้ง (เส้นทางใหม่ ชังก์ใหม่) และถูกแทนที่ด้วย fallback อย่างกะทันหัน ตั้งแต่ React 18 การอัปเดตที่ห่อไว้ใน startTransition จะเปลี่ยนพฤติกรรมนี้: React คงอินเทอร์เฟซเดิมไว้บนหน้าจอ ในขณะที่การเรนเดอร์ใหม่พักอยู่เบื้องหลัง และแสดง fallback เฉพาะกับเนื้อหาที่ไม่เคยถูกเปิดเผยมาก่อนเท่านั้น ขอบเขต (boundaries) ยังประกอบซ้อนกันในเชิงลึกได้อีกด้วย: ขอบเขต <Suspense> ที่ซ้อนกันหลายชั้นช่วยให้คุณเปิดเผยอินเทอร์เฟซทีละโซน ตั้งแต่โครงร่างระดับรวม (skeleton) ลงไปจนถึงรายละเอียด

JSX
const [isPending, startTransition] = useTransition();

function openTab(nextTab) {
  startTransition(() => {
    setTab(nextTab);
  });
}
// The current tab stays visible while the new
// one loads, instead of dropping back to the fallback.
transition ช่วยหลีกเลี่ยงการตกกลับไปที่ fallback ระหว่างการนำทาง

ทดสอบความรู้

ตรวจสอบว่าคุณจำประเด็นสำคัญของบทเรียนนี้ได้ครบถ้วน

  1. ฟังก์ชันที่ส่งให้ React.lazy ต้องคืนค่าอะไร?
    • คอมโพเนนต์ React โดยตรง
    • promise ที่คลี่คลายไปเป็นโมดูลที่มี export เป็น default
    • อ็อบเจกต์ { component, loading }
    • สตริงที่มีพาธของไฟล์
  2. คุณจะป้องกันไม่ให้เนื้อหาที่แสดงอยู่แล้วถูกแทนที่ด้วย fallback เมื่อการนำทางพักได้อย่างไร?
    • ห่อการอัปเดตสเตตไว้ใน startTransition
    • ส่ง fallback={null} ให้ขอบเขต Suspense
    • ใช้ useEffect เพื่อหน่วงการนำทาง
    • ลบขอบเขต Suspense ที่เป็นพาเรนต์ออก
  3. จริง ๆ แล้วใครเป็นผู้ทำการแบ่งโค้ดออกเป็นชังก์แยกต่างหาก?
    • React ระหว่างเฟสคอมมิต
    • เบราว์เซอร์ ผ่านแคช HTTP
    • บันเดิลเลอร์ จากแต่ละ import() แบบไดนามิก