Code Splitting: دید کلی‌تر بر تقسیم بسته جاوااسکریپت

در دو درس قبل، با Dynamic Import و تابع lazy ابزارهای پایه را دیدیم. در این درس، یک قدم عقب‌تر می‌رویم تا Code Splitting را به‌عنوان یک استراتژی کلی، نه فقط یک تکنیک نقطه‌ای، ببینیم: برنامه را بر چه اساس‌هایی می‌توان تقسیم کرد و چرا این تقسیم‌بندی، همیشه به یک شکل انجام نمی‌شود.

یادآوری مسئله اصلی

بدون هیچ تقسیم‌بندی، ابزار بسته‌بندی (Bundler) تمام کد برنامه را در یک یا چند فایل بزرگ جمع می‌کند که همگی در بارگذاری اولیه دانلود می‌شوند. Code Splitting، یعنی شکستن این بسته به تکه‌های کوچک‌تر که هرکدام جداگانه و در زمان مناسب خودشان بارگذاری می‌شوند.

مرز اول: بر اساس مسیر (Route-based)

همان‌طور که در فصل پانزدهم دیدیم، رایج‌ترین و طبیعی‌ترین مرز تقسیم، خود صفحات برنامه است. کاربری که وارد صفحه خانه می‌شود، نباید مجبور باشد کد صفحه تنظیمات یا داشبورد را هم دانلود کند.

jsx
const SettingsPage = lazy(() => import("./SettingsPage"));

مرز دوم: بر اساس تعامل کاربر (Interaction-based)

گاهی یک بخش از یک صفحه، نه یک صفحه کامل، فقط پس از یک تعامل خاص لازم می‌شود؛ مثل یک Modal پیچیده که فقط با کلیک روی یک دکمه باز می‌شود:

jsx
const EditorModal = lazy(() => import("./EditorModal"));

function Toolbar() {
  const [showEditor, setShowEditor] = useState(false);
  return (
    <div>
      <button onClick={() => setShowEditor(true)}>باز کردن ویرایشگر</button>
      {showEditor && (
        <Suspense fallback={<p>در حال بارگذاری...</p>}>
          <EditorModal />
        </Suspense>
      )}
    </div>
  );
}

در این الگو، حتی اگر کاربر هرگز روی دکمه کلیک نکند، کد EditorModal اصلاً دانلود نمی‌شود.

مرز سوم: بر اساس کتابخانه‌های سنگین

گاهی خود یک کامپوننت کوچک است، اما به یک کتابخانه بیرونی حجیم وابسته است (مثلاً یک کتابخانه رسم نمودار یا ویرایش متن پیشرفته). جدا کردن دقیقاً همان بخشی که این کتابخانه را وارد می‌کند، بدون نیاز به lazy کردن کل آن صفحه، می‌تواند هدفمندتر باشد.

تقسیم بیش‌ازحد، خودش یک هزینه دارد

هر تکه جدا، یک درخواست شبکه جداگانه است. درست مثل هر بهینه‌سازی دیگری که در این دوره دیدیم (useMemo در فصل سیزدهم، به‌خصوص)، تقسیم بیش‌ازحد ریز، می‌تواند تعداد درخواست‌های کوچک را آن‌قدر زیاد کند که سربار شبکه (هر درخواست، هزینه ثابتی هم دارد)، از فایده کاهش حجم، بیشتر شود. معمولاً بهتر است تقسیم‌بندی در سطح معقولی از درشتی (مثل کل یک صفحه یا یک Modal کامل) انجام شود، نه هر تک کامپوننت کوچک.

تحلیل بسته با ابزارهای Build

اکثر ابزارهای Build مدرن (مثل Vite یا Webpack)، گزینه‌ای برای تولید یک گزارش بصری از حجم هر بخش بسته نهایی دارند. درست مثل Profiler در فصل سیزدهم که برای Re-render استفاده کردیم، این گزارش‌ها کمک می‌کنند تصمیم تقسیم‌بندی بر اساس داده واقعی گرفته شود، نه حدس زدن کدام بخش بزرگ است.

مثال قابل اجرا

در این مثال، یک Modal فقط پس از کلیک کاربر، lazy بارگذاری می‌شود؛ نمونه‌ای از مرز تقسیم بر اساس تعامل.

تقسیم کد بر اساس یک تعامل کاربر، نه یک مسیر
import { useState, lazy, Suspense } from "react";

const EditorModal = lazy(() => {
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve({
        default: function EditorModal() {
          return <div style={{ border: "1px solid gray", padding: "10px" }}>ویرایشگر پیشرفته</div>;
        },
      });
    }, 900);
  });
});

function App() {
  const [showEditor, setShowEditor] = useState(false);

  return (
    <div>
      <button onClick={() => setShowEditor(true)}>باز کردن ویرایشگر</button>
      {showEditor && (
        <Suspense fallback={<p>در حال بارگذاری ویرایشگر...</p>}>
          <EditorModal />
        </Suspense>
      )}
    </div>
  );
}
برای دیدن پیش‌نمایش کامپوننت، روی Run بزنید.

جمع‌بندی

Code Splitting را می‌توان بر اساس مسیر، تعامل کاربر، یا وابستگی به یک کتابخانه سنگین انجام داد؛ انتخاب مرز درست، به ساختار واقعی برنامه بستگی دارد. تقسیم بیش‌ازحد ریز، سربار شبکه اضافه می‌کند، و ابزارهای تحلیل بسته، کمک می‌کنند این تصمیم بر پایه داده واقعی گرفته شود. در درس بعدی، به‌طور مشخص به Route-based Splitting و رابطه آن با Routing برمی‌گردیم.