چطور Baseline کمک می‌کند JavaScript کمتری بفرستیم؟

چطور Baseline کمک می‌کند JavaScript کمتری بفرستیم؟

شهریور ۱۵, ۱۴۰۵

فاصلهٔ «کتابخانه» و «خود مرورگر» کوتاه‌تر شده

معمولاً بیشتر وابستگی‌های package.json یک‌بار نصب می‌شوند، تست‌ها پاس می‌شوند و پرونده بسته می‌شود. بعد هم کمتر کسی دوباره سراغشان می‌رود.

اما مرورگرها همان‌جا متوقف نشده‌اند. فرمت تاریخ و عدد، درخواست HTTP، مودال، تولتیپ، deep clone و حتی گروه‌بندی آرایه‌ها حالا در بسیاری از موارد بدون نیاز به یک پکیج جداگانه قابل انجام‌اند.

در یک اپلیکیشن متوسط جاوااسکریپتی، گاهی ۶۰ تا ۹۰ کیلوبایت (minified و gzipped) فقط صرف وابستگی‌هایی می‌شود که مرورگر خودش می‌تواند بخش زیادی از کارشان را انجام دهد. البته مسئله تنبلی یا بی‌دقتی تیم نیست. وقتی یک قابلیت جدید می‌آید، تیم‌ها معمولاً ترجیح می‌دهند برای چیزی که امتحانش را پس داده، ریسک نکنند.

حذف وابستگی‌های npm بعد از جایگزینی با قابلیت‌های پلتفرم

Baseline چیست؟

Baseline که توسط WebDX Community Group شکل گرفته، در ساده‌ترین تعریف می‌گوید یک قابلیت وب در Chrome، Edge، Firefox و Safari چقدر قابل اتکاست.

این یعنی به‌جای اینکه فقط بپرسیم «کدام نسخهٔ مرورگر را پشتیبانی می‌کنیم؟»، یک سؤال کاربردی‌تر هم داریم:

«این قابلیت Baseline هست یا نه؟»

Baseline سه وضعیت اصلی دارد:

  • Limited availability — قابلیت هنوز در همهٔ موتورهای اصلی وب در دسترس نیست. پس اگر از آن استفاده می‌کنید، باید fallback داشته باشید.
  • Baseline Newly available — قابلیت به‌تازگی در موتورهای اصلی در دسترس قرار گرفته و روی مرورگرهای به‌روز کار می‌کند، اما ممکن است بخشی از کاربران با مرورگرهای قدیمی هنوز آن را نداشته باشند.
  • Baseline Widely available — قابلیت حدود سی ماه است که در موتورهای اصلی وجود دارد و معمولاً می‌توانید با خیال راحت‌تری روی آن حساب کنید.

فاصلهٔ بین Newly و Widely را دست‌کم نگیرید. برای قابلیت‌های اصلی اپ بهتر است سراغ موارد Widely بروید. قابلیت‌های Newly را هم می‌توانید با progressive enhancement و ابزارهایی مثل @supports یا feature check به‌صورت مرحله‌ای اضافه کنید.

وضعیت هر قابلیت را می‌توانید در webstatus.dev، MDN یا پکیج web-features بررسی کنید.

قبل از حذف: سه سؤال

قبل از اینکه یک dependency را پاک کنید، سه سؤال ساده بپرسید:

  1. جایگزین برای مخاطب من Baseline-safe است؟ اگر Widely باشد، معمولاً جواب مثبت است. برای Newly بهتر است analytics یا browserslist را هم بررسی کنید.

  2. هزینهٔ واقعی این جابه‌جایی چقدر است؟ ممکن است یک polyfill از خود کتابخانه‌ای که حذف می‌کنید سنگین‌تر باشد. در این حالت نه‌تنها چیزی ذخیره نکرده‌اید، بلکه باندل را هم بزرگ‌تر کرده‌اید؛ مگر اینکه polyfill را conditional load کنید. (البته این موضوع به نسخهٔ جاوااسکریپت کلاینت هم بستگی دارد.)

  3. قابلیت native واقعاً همان use case من را پوشش می‌دهد؟ مثلاً axios فقط یک wrapper دور fetch نیست. interceptor، retry و امکانات دیگری هم دارد که ممکن است اپ شما واقعاً به آن‌ها وابسته باشد.

در ادامه، برای هر حوزه همین سه سؤال را در نظر می‌گیریم.


حوزه ۱: بین‌المللی‌سازی

در این بخش معمولاً می‌توان بیشترین مقدار کد را بدون دردسر کنار گذاشت. مرورگرها مدت‌هاست زیر مجموعهٔ Intl امکانات کاملی برای فرمت کردن تاریخ، عدد و متن دارند و در نتیجه خیلی از کتابخانه‌های کوچک دیگر ضروری نیستند.

چند مظنون همیشگی و جایگزین native آن‌ها:

  • timeago.js (~۱KB gz) → Intl.RelativeTimeFormat
  • pluralize (~۲.۳KB gz) → Intl.PluralRules
  • numeral (~۳.۹KB gz) → Intl.NumberFormat
  • humanize-duration (~۶.۶KB gz) → Intl.DurationFormat
  • helperهای مربوط به join کردن لیست → Intl.ListFormat

زمان نسبی

timeago.js یک timestamp را به چیزی مثل «۳ ساعت پیش» تبدیل می‌کند. Intl.RelativeTimeFormat هم همین کار را انجام می‌دهد و Baseline Widely available است:

const rtf = new Intl.RelativeTimeFormat("en", { numeric: "auto" });

rtf.format(-1, "day"); // "yesterday"
rtf.format(3, "hour"); // "in 3 hours"
rtf.format(-2, "week"); // "2 weeks ago"

نکتهٔ جالب numeric: "auto" است. اگر زبان برای یک حالت خاص کلمهٔ مشخصی داشته باشد، به‌جای عبارت عددی از همان کلمه استفاده می‌کند؛ مثلاً yesterday به‌جای 1 day ago.

البته یک تفاوت کوچک وجود دارد. timeago.js خودش از روی اختلاف دو تاریخ تصمیم می‌گیرد که مثلاً باید «ثانیه» بگویید یا «روز». RelativeTimeFormat این بخش را به عهدهٔ شما می‌گذارد.

پس باید چند خط helper بنویسید که اختلاف تاریخ را حساب کند و مناسب‌ترین واحد را انتخاب کند. بعد از آن، برای همین کار دیگر به کتابخانه نیاز ندارید.

عدد، ارز و لیست

Intl.NumberFormat بخش بزرگی از کار کتابخانه‌های فرمت عدد را پوشش می‌دهد: جداکنندهٔ هزارگان، ارز، درصد و حتی compact notation:

new Intl.NumberFormat("en-US").format(1234567.89);
// "1,234,567.89"

new Intl.NumberFormat("en-US", {
  style: "currency",
  currency: "USD"
}).format(1234.5);
// "$1,234.50"

new Intl.NumberFormat("en", {
  notation: "compact"
}).format(1200000);
// "1.2M"

برای لیست‌ها هم Intl.ListFormat — که Widely available است — کار را خیلی تمیزتر می‌کند. مثلاً:

const lf = new Intl.ListFormat("en", {
  style: "long",
  type: "conjunction"
});

lf.format(["Alice", "Bob", "Carol"]);
// "Alice, Bob, and Carol"

یعنی دیگر لازم نیست برای تبدیل آرایه‌ای از نام‌ها به یک جمله، helper مخصوص خودمان بنویسیم.

یک caveat: مدت زمان

humanize-duration میلی‌ثانیه را به عبارتی مثل 1 hour, 30 minutes تبدیل می‌کند. معادل native آن Intl.DurationFormat است:

const df = new Intl.DurationFormat("en", {
  style: "long"
});

df.format({ hours: 1, minutes: 30 });
// "1 hour, 30 minutes"

اما اینجا باید کمی صبر کنید.

Intl.DurationFormat هنوز Newly available است، نه Widely. این قابلیت در مارس ۲۰۲۵ به همهٔ موتورهای اصلی رسید و مسیر Widely شدنش حدود ۲۰۲۷ است.

برای یک اپ با مخاطب گسترده، این یعنی هنوز سؤال اول را نمی‌توانیم به‌سادگی با «بله» جواب بدهیم. اگر ترافیک شما تقریباً کاملاً روی مرورگرهای جدید است، داستان فرق می‌کند؛ وگرنه بهتر است feature check یا fallback داشته باشید.

پس استفاده از API native روی مرورگرهای مدرن امروز کاملاً منطقی است، اما برای یک سایت عمومی که هنوز کاربران با دستگاه‌های قدیمی دارد، شاید بهتر باشد فعلاً کمی صبر کنید.

اگر هر چهار کتابخانهٔ humanize-duration، timeago.js، pluralize و numeral را داشته باشید، مجموعشان حدود ۱۴KB gzip می‌شود. بخش قابل‌توجهی از این حجم را همین حالا می‌توان با APIهای Widely جایگزین کرد.


حوزه ۲: کلاینت‌های HTTP

اینجا داستان کمی متفاوت است و بهتر است عجله نکنید.

دو نمونهٔ رایج در مرورگر، axios با حدود ۱۷KB gz و superagent با حدود ۱۹KB gz هستند. برای بسیاری از درخواست‌های معمولی، fetch به‌همراه AbortController کاملاً کافی است و هر دو API هم Widely available هستند.

مثلاً یک GET ساده در axios:

const { data } = await axios.get("/api/users");

با fetch می‌شود:

const res = await fetch("/api/users");
const data = await res.json();

این یک خط اضافه، یعنی res.json()، در واقع نشان‌دهندهٔ تفاوت فلسفهٔ این دو API است. fetch قرار نیست همه‌چیز را برای شما انجام دهد. ابزارهای پایه را می‌دهد و تصمیم‌های بعدی را به خودتان واگذار می‌کند.

Timeout

axios گزینهٔ timeout دارد. در fetch می‌توانید از AbortSignal.timeout() استفاده کنید:

const res = await fetch("/api/users", {
  signal: AbortSignal.timeout(5000), // abort بعد از ۵ ثانیه
});

جایی که fetch جای axios را نمی‌گیرد

اینجا سؤال سوم از آن سه سؤال قبلی اهمیت بیشتری پیدا می‌کند: آیا واقعاً قابلیت‌های موردنیاز شما در fetch وجود دارد؟

چند تفاوت مهم:

  • خطای HTTP: fetch برای پاسخ‌هایی مثل 404 یا 500 promise را reject نمی‌کند. این پاسخ‌ها از نظر fetch همچنان یک response معتبرند. بنابراین باید خودتان res.ok را بررسی کنید. axios در وضعیت‌های غیر ۲xx به‌طور پیش‌فرض reject می‌کند.
  • Interceptor: fetch چیزی معادل آماده برای interceptor ندارد. اگر برای اضافه کردن auth token یا مدیریت پاسخ‌های 401 به interceptor وابسته‌اید، باید wrapper یا class خودتان را بنویسید.
  • Retry: fetch retry خودکار ندارد. اگر چنین رفتاری می‌خواهید، باید خودتان آن را پیاده کنید یا از یک ابزار دیگر کمک بگیرید.
  • Upload progress: برای آپلود فایل با progress bar، fetch هنوز به اندازهٔ بعضی کتابخانه‌ها امکانات مستقیم و راحتی ندارد. در چنین پروژه‌ای نگه داشتن کتابخانه می‌تواند کاملاً منطقی باشد.

هیچ‌کدام از این قابلیت‌ها غیرممکن نیستند و ساختنشان هم لزوماً سخت نیست. اما نکته این است که نباید صرفاً به‌خاطر چند کیلوبایت، abstractionهایی را که واقعاً استفاده می‌کنید دوباره بسازید.

بنابراین این دقیقاً جایی است که find-and-replace کورکورانه جواب نمی‌دهد. اول ببینید HTTP client در پروژهٔ شما دقیقاً چطور استفاده شده است.

اگر فقط GET و POST ساده دارید، یک wrapper کوچک روی fetch می‌تواند حدود ۱۷KB gzip برایتان صرفه‌جویی کند.


حوزه ۳: UI primitives

بعضی از بهترین جایگزینی‌ها را می‌شود در همین بخش پیدا کرد؛ مخصوصاً چون بعضی APIهای جدید مرورگر نه‌تنها جای کتابخانه را می‌گیرند، بلکه accessibility بهتری هم نسبت به راه‌حل‌های دستی ارائه می‌دهند.

چند نمونه:

  • مودال، مثل a11y-dialog (~۱.۸KB gz)
  • تولتیپ و popover، مثل tippy.js (~۱۴KB gz، به‌همراه Popper برای positioning)
  • focus-trap (~۶.۶KB gz)
  • body-scroll-lock (~۱.۳KB gz)

و در سمت پلتفرم، سه ابزار مهم داریم:

<dialog>، Popover API و CSS anchor positioning.

عنصر <dialog>

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

<dialog> که Widely available است، بخش زیادی از این کارها را خودش انجام می‌دهد:

<dialog id="confirm">
  <form method="dialog">
    <p>Delete this file?</p>
    <button value="cancel">Cancel</button>
    <button value="delete">Delete</button>
  </form>
</dialog>

و برای باز کردن آن:

const dialog = document.querySelector("#confirm");

dialog.showModal();
// فوکوس وارد مودال می‌شود، پس‌زمینه inert می‌شود و Escape مودال را می‌بندد

dialog.addEventListener("close", () => {
  console.log(dialog.returnValue); // "cancel" یا "delete"
});

showModal() همان بخشی از کار را انجام می‌دهد که قبلاً شاید برایش focus-trap نصب می‌کردید. focus داخل dialog می‌ماند، محتوای پشت آن inert می‌شود، Escape کار می‌کند و بعد از بسته شدن، focus به عنصر قبلی برمی‌گردد.

از طرف دیگر، dialog در Top Layer مرورگر قرار می‌گیرد؛ بنابراین دیگر لازم نیست برای اینکه مودال بالاتر از همه‌چیز باشد با z-index کلنجار بروید.

برای overlay هم ::backdrop را دارید.

در نتیجه، یک <dialog> می‌تواند جای مودال و focus-trap را بگیرد.

تنها چیزی که خودش حل نمی‌کند، قفل کردن اسکرول پس‌زمینه است؛ یعنی همان کاری که body-scroll-lock انجام می‌داد. برای این مورد می‌توانید از CSS استفاده کنید:

body:has(dialog:modal) {
  overflow: hidden;
}

شاید بپرسید چرا dialog:modal و نه dialog[open]؟

چون attribute به نام open با show() هم تنظیم می‌شود، در حالی که در آن حالت dialog هنوز modal نیست. :modal دقیقاً حالتی را هدف می‌گیرد که dialog واقعاً با showModal() باز شده است.

یعنی در نهایت، سه کتابخانه را با یک عنصر HTML و یک قانون CSS کنار می‌گذارید.

Popover API و anchor positioning

همه‌چیز هم قرار نیست یک modal باشد.

برای منوی کشویی، tooltip یا پنل‌های کوچک شناور، Popover API می‌تواند بخش زیادی از رفتاری را که قبلاً با tippy.js و JavaScript می‌ساختیم، خودش انجام دهد.

مثلاً:

<button popovertarget="menu" id="options">Options</button>

<div id="menu" popover>
  <!-- محتوای منو -->
</div>

با کلیک روی دکمه، popover باز می‌شود؛ کلیک بیرون آن را می‌بندد و Escape هم رفتار مورد انتظار را دارد.

Popover API از ژانویهٔ ۲۰۲۵ Baseline Newly available است.

اما تولتیپ فقط به باز و بسته شدن نیاز ندارد. باید بتواند خودش را کنار trigger قرار دهد و اگر از viewport بیرون رفت، جای مناسبی پیدا کند. این همان بخشی است که Popper در tippy.js انجام می‌دهد.

حالا CSS قابلیت دیگری به نام anchor positioning دارد:

#options {
  anchor-name: --trigger;
}

.tooltip {
  position-anchor: --trigger;
  position-area: top;
  margin: 0;
}

این قابلیت هنوز خیلی جدید است. Anchor positioning در ژانویهٔ ۲۰۲۶ به Baseline Newly available رسید؛ زمانی که Firefox 147 منتشر شد. Chrome از نسخهٔ ۱۲۵ و Safari از نسخهٔ ۲۶ آن را داشتند.

پس اینجا دوباره باید همان سؤال اول را بپرسید: کاربران شما چه مرورگرهایی دارند؟

برای یک اپ مدرن می‌تواند انتخاب بسیار خوبی باشد، اما بهتر است قبل از حذف کامل fallbackهای قدیمی، analytics را بررسی کنید. ضمن اینکه قابلیت‌های پیشرفته‌تر، مثل position-try و fallbackهای مربوط به آن، هنوز بین نسخه‌های مختلف مرورگر کاملاً یکدست نیستند.

در مجموع، <dialog>، Popover API و anchor positioning می‌توانند در حوزهٔ UI جای بخشی از کدهای مربوط به modal، tooltip، focus-trap و body-scroll-lock را بگیرند. این بخش در مجموع حدود ۲۴KB gzip است و در بسیاری از موارد accessibility بهتری هم نسبت به راه‌حل‌های دستی دارد.


حوزه ۴: ابزارهای Lodash

امروزه کمتر کسی کل Lodash را یک‌جا import می‌کند، اما توابع جداگانهٔ آن هنوز در خیلی از پروژه‌ها دیده می‌شوند.

گاهی کل پکیج lodash (~۲۵KB gz) وارد پروژه شده و گاهی فقط چیزهایی مثل lodash.clonedeep یا lodash.groupby.

چند مورد از این توابع حالا معادل مستقیم native دارند.

گروه‌بندی

lodash.groupby یک آرایه را بر اساس یک property گروه‌بندی می‌کند. Object.groupBy تقریباً دقیقاً همین کار را انجام می‌دهد:

const products = [
  { name: "Apple", category: "fruit" },
  { name: "Carrot", category: "vegetable" },
  { name: "Banana", category: "fruit" },
];

const grouped = Object.groupBy(
  products,
  (product) => product.category
);

// {
//   fruit: [
//     { name: "Apple", ... },
//     { name: "Banana", ... }
//   ],
//   vegetable: [
//     { name: "Carrot", ... }
//   ]
// }

اگر به‌جای object به Map نیاز دارید، Map.groupBy هم وجود دارد؛ مخصوصاً وقتی کلیدها string نیستند.

هر دو API از مارس ۲۰۲۴ Newly available هستند و مسیر Widely شدنشان حدود اواخر ۲۰۲۶ است. بنابراین فعلاً مثل همیشه بهتر است وضعیت مرورگرهای مخاطب را بررسی کنید.

کپی عمیق

lodash.clonedeep برای ساختن deep copy استفاده می‌شود. جایگزین native آن structuredClone است و Baseline Widely available دارد:

const original = {
  user: {
    name: "Sam",
    roles: ["admin"]
  }
};

const copy = structuredClone(original);

copy.user.roles.push("editor");

original.user.roles;
// ["admin"]

structuredClone برخلاف روش قدیمی JSON.parse(JSON.stringify(...)) می‌تواند مواردی مثل Date، Map، Set، ArrayBuffer و referenceهای چرخه‌ای را هم درست clone کند.

البته محدودیت‌هایی دارد. تابع، DOM node و instanceهای کلاس را مثل دادهٔ ساده clone نمی‌کند. مثلاً برای تابع خطا می‌دهد و در مورد instance کلاس هم prototype را حفظ نمی‌کند.

اما اگر چیزی که clone می‌کنید عمدتاً plain data است، structuredClone جایگزین تمیزی برای lodash.clonedeep است.

عملیات Set

اگر برای union، intersection یا difference یک helper از Lodash گرفته‌اید، شاید دیگر نیازی به آن نداشته باشید.

خود Set حالا این عملیات را دارد:

const admins = new Set(["sam", "alex", "jo"]);
const editors = new Set(["alex", "kim"]);

admins.intersection(editors);
// Set { "alex" }

admins.union(editors);
// Set { "sam", "alex", "jo", "kim" }

admins.difference(editors);
// Set { "sam", "jo" }

مجموعهٔ این متدها شامل موارد زیر است:

union، intersection، difference، symmetricDifference، isSubsetOf، isSupersetOf و isDisjointFrom.

این قابلیت‌ها از ژوئن ۲۰۲۴ Newly available هستند.

چه چیزی را نگه دارید؟

قرار نیست نتیجه بگیریم که «Lodash دیگر لازم نیست».

مثلاً debounce و throttle هنوز معادل native مستقیمی ندارند و همچنان می‌توانند مفید باشند. بنابراین استفادهٔ مستقیم از lodash.debounce کاملاً منطقی است.

هدف این نیست که هرچه Lodash دارید پاک کنید. هدف این است که برای قابلیتی که مرورگر خودش ارائه می‌دهد، کدی را که قبلاً برایش dependency آورده‌ایم دوباره نفرستیم.

فقط حذف lodash.clonedeep و lodash.groupby می‌تواند حدود ۸KB gzip صرفه‌جویی کند. اگر کل Lodash را فقط برای چند تابع وارد کرده‌اید، جایگزین کردن همان چند تابع ممکن است باعث شود کل پکیج را هم حذف کنید.


حوزه ۵: Temporal — الان حذف نکنید

چهار حوزهٔ قبلی تقریباً به یک نتیجه می‌رسیدند: «اگر شرایطش را دارید، حذف کنید.»

اما Temporal مثال خوبی است برای زمانی که جواب درست دقیقاً برعکس است: فعلاً دست نگه دارید.

Temporal قرار است جایگزین مدرن‌تری برای Date باشد و API بسیار بهتری ارائه می‌دهد: objectهای immutable، مدیریت مناسب‌تر time zone و رفع مشکلات آشنایی مثل شروع شدن ماه‌ها از صفر.

در مارس ۲۰۲۶، Temporal به Stage 4 در TC39 رسید و بخشی از مشخصات ES2026 شد.

Firefox از نسخهٔ ۱۳۹ در سال ۲۰۲۵ و Chrome از نسخهٔ ۱۴۴ در ژانویهٔ ۲۰۲۶ آن را ارائه کرده‌اند. Safari هنوز در نسخهٔ stable پشتیبانی نمی‌کند و قابلیت فعلاً در Technology Preview آن قرار دارد؛ انتظار می‌رود در اواخر ۲۰۲۶ به نسخهٔ stable برسد.

پس با اینکه Temporal از نظر API جذاب است، هنوز Baseline نیست. وضعیت فعلی آن Limited availability است و دلیل اصلی هم نبود پشتیبانی stable در Safari است.

اگر امروز بخواهید برای همهٔ مرورگرها از Temporal استفاده کنید، به polyfill نیاز دارید.

اینجاست که حساب‌وکتاب جالب می‌شود.

polyfill رسمی @js-temporal/polyfill حدود ۴۴KB gzip حجم دارد. نسخهٔ سبک‌تر که به BigInt وابسته نیست حدود ۱۹KB gzip است. در مقابل، کتابخانه‌ای مثل dayjs حدود ۳KB gzip است.

بنابراین اگر همین امروز dayjs را حذف کنید و Temporal را همراه با polyfill وارد باندل کنید، نه‌تنها ۳KB ذخیره نکرده‌اید، بلکه ممکن است حدود ۴۱KB به باندل اضافه کرده باشید؛ مگر اینکه polyfill را به‌صورت شرطی load کنید.

دوباره همان سه سؤال را بررسی کنیم:

  • مخاطب: Temporal هنوز Baseline نیست و برای یک سایت عمومی یعنی بخشی از کاربران آن را ندارند.
  • هزینه: polyfill آن بسیار بزرگ‌تر از کتابخانه‌ای است که قرار است حذف کنید.
  • شکاف قابلیت: Temporal از نظر API واقعاً بهتر است و امکانات بیشتری نسبت به dayjs می‌دهد، اما تا وقتی دو سؤال اول جواب مناسبی ندارند، این برتری به‌تنهایی کافی نیست.

بنابراین تصمیم فعلی معمولاً ساده است: dayjs یا date-fns را نگه دارید.

زمان مناسب برای بازبینی وقتی است که Safari نسخهٔ stable را منتشر کند و Temporal به Baseline برسد. آن زمان می‌توانید سراغ API native بروید و در صورت نیاز polyfill را فقط برای کاربران قدیمی‌تر به‌صورت شرطی load کنید.

Temporal را در لیست کارهای آینده نگه دارید؛ فعلاً چیزی نیست که لازم باشد همین امروز برایش dependency فعلی را حذف کنید.


چطور روی package.json خودتان audit کنید؟

بررسی اندازهٔ پکیج در Bundlephobia

برای اینکه ببینید در پروژهٔ خودتان چه چیزهایی قابل حذف‌اند، می‌توانید از یک روند ساده شروع کنید.

۱. dependencyهای production را ببینید

npm ls --omit=dev --depth=0

۲. هزینهٔ واقعی هر dependency را اندازه بگیرید

برای یک عدد سریع، Bundlephobia بد نیست. اما اگر می‌خواهید بدانید واقعاً چه چیزی بعد از tree-shaking وارد باندل شما می‌شود، بهتر است از analyzerهایی مثل source-map-explorer یا vite-bundle-visualizer استفاده کنید.

۳. جایگزین native و وضعیت Baseline را بررسی کنید

برای هر dependency که به نظر می‌رسد قابل حذف است، جایگزین پلتفرمی آن را پیدا کنید و وضعیتش را در webstatus.dev یا MDN ببینید.

۴. همان سه سؤال را دوباره بپرسید

مخاطب؟ هزینه؟ شکاف قابلیت؟

اگر هر سه جواب مناسبی داشتند، احتمالاً ارزش دارد dependency را حذف کنید.

۵. برای قابلیت‌های Newly از progressive enhancement استفاده کنید

مثلاً برای Intl.DurationFormat:

if (typeof Intl.DurationFormat === "function") {
  // پلتفرم
} else {
  // کتابخانه یا فرمت ساده‌تر
}

این روش اجازه می‌دهد روی مرورگرهای جدید از قابلیت native استفاده کنید، بدون اینکه کاربران قدیمی را مجبور کنید با صفحه‌ای خراب یا بدون fallback روبه‌رو شوند.

جمع‌بندی

اگر یک اپ متوسط جاوااسکریپتی داشته باشید، چند دسته dependency هستند که ارزش بررسی جدی دارند:

حوزه کاهش تقریبی (gzip)
بین‌المللی‌سازی ~۱۴KB
HTTP ~۱۷KB
UI primitives ~۲۴KB
Lodash (بخش‌های رایج) ~۸KB یا بیشتر
جمع معمول ~۶۰–۹۰KB

اعدادی که در analyzer به‌صورت uncompressed می‌بینید معمولاً دو تا سه برابر بزرگ‌ترند. بعضی پکیج‌های dialog حتی به‌تنهایی می‌توانند حدود ۵۰KB gz باشند.

اما نکتهٔ اصلی فقط چند عدد در جدول نیست.

مرورگر مدام در حال اضافه کردن قابلیت‌هایی است که قبلاً مجبور بودیم برایشان dependency نصب کنیم. بنابراین audit کردن package.json نباید یک پروژهٔ یک‌باره باشد.

هر فصل یک audit کوچک انجام دهید: dependencyها را مرور کنید، وضعیت Baseline را چک کنید و ببینید آیا پلتفرم حالا کاری را که قبلاً یک کتابخانه برایتان انجام می‌داد، خودش انجام می‌دهد یا نه.

نمای کلی جایگزینی کتابخانه‌ها با قابلیت‌های پلتفرم

در یک اپ متوسط، مجموع چیزی که امروز می‌توان بدون Temporal از dependencyها کنار گذاشت، معمولاً حدود ۶۰ تا ۹۰KB gzip است.

و شاید بهترین سؤال برای شروع audit بعدی همین باشد:

«این dependency هنوز کاری انجام می‌دهد که خود مرورگر نتواند انجامش دهد؟»


منابع

[1] Jad Joubran, “How Baseline Can Help You Ship Less JavaScript,” Smashing Magazine, Aug. 2026.

[2] “Baseline: A New Way to Think About Browser Support,” OpenReplay Blog, Feb. 13, 2026.