مرتب سازی تاریخ در جاوااسکریپت یکی از چالش‌های رایج در توسعه اپلیکیشن‌های تحت وب محسوب می‌شود؛ به‌ویژه زمانی که نیاز به مرتب سازی آرایه‌ای از آبجکت‌ها بر اساس فیلد تاریخ داریم. معمولاً این تاریخ‌ها در قالب استاندارد ISO 8601 (مانند "2025-05-01T15:00:00.00") ذخیره می‌شوند؛ قالبی که به‌طور گسترده در APIها و پایگاه‌های داده مورد استفاده قرار می‌گیرد و منطقه زمانی در آن مشخص نشده است.

یک روش رایج برای انجام این کار، استفاده از متد sort() همراه با تبدیل رشته‌های تاریخ به آبجکت‌های Date است:

const sorted = data.sort((a, b) => {
  return new Date(a.date) - new Date(b.date);
})

این روش برای مجموعه‌داده‌های کوچک عملکرد قابل قبولی دارد. اما در شرایطی که آرایه شامل تعداد زیادی آیتم (برای مثال ۳۰٬۰۰۰ آبجکت) باشد، زمان اجرای عملیات به‌طور محسوس افزایش می‌یابد. در یک سیستم توسعه نسبتاً سریع، این تأخیر حدود ۱۰۰ تا ۱۵۰ میلی‌ثانیه است که در صورت ترکیب با سایر فرآیندهای رابط کاربری، کاملاً محسوس خواهد بود.

آزمایش‌هایی که با شبیه‌سازی کاهش توان پردازنده (CPU Throttling) با ضریب ۴ در مرورگر انجام دادیم، نشان می‌دهند که این زمان به حدود ۴۰۰ میلی‌ثانیه افزایش یافته است. چنین شبیه‌سازی‌هایی، تصویری واقع‌بینانه‌تر از عملکرد برنامه روی دستگاه‌های ضعیف‌تر ارائه می‌دهند و کمک می‌کنند تا از کیفیت تجربه کاربری در طیف وسیعی از سخت‌افزارها اطمینان حاصل شود.

نتیجه در مرورگر:

sort_with_date_conversion: 397.955078125 ms

خروجی حاصل از اجرای عملیات در شرایط شبیه‌سازی شده که عملکرد مرورگر را تا ۴ برابر کاهش می‌دهد.

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

چرا ۴۰۰ میلی‌ثانیه می‌تواند احساس کندی ایجاد کند؟

بر اساس نظریه یاکوب نیلسن در کتاب کلاسیک Usability Engineering که او در سال ۱۹۹۳ منتشر کرده است، تأخیرهایی که کم‌تر از ۱۰۰ میلی‌ثانیه هستند، برای کاربر آنی به نظر می‌رسند. اما اگر این تأخیر بین ۱۰۰ تا ۱۰۰۰ میلی‌ثانیه باشد، کاربر آن را به عنوان لگ یا کندی درک می‌کند، حتی اگر هیچ بازخورد بصری در رابط کاربری نمایش داده نشود.

در محیط‌هایی که یک کامپوننت یا ماژول در حال انجام چندین عملیات هم‌زمان است، مانند کامپوننت‌های PCF، تأخیری در حدود ۴۰۰ میلی‌ثانیه می‌تواند تجربه کاربری را تحت تأثیر قرار دهد. در چنین شرایطی، توسعه‌دهندگان باید از روش‌های مؤثرتری برای بهینه‌سازی عملکرد استفاده کنند.

تنظیم یک آزمایش برای ارزیابی عملکرد

برای بررسی دقیق‌تر این مسئله، یک آزمایش ساده طراحی می‌کنیم که هدف آن ارزیابی عملکرد مرتب‌سازی تحت فشار پردازشی است. در این آزمایش، آرایه‌ای شامل ۱۰۰٬۰۰۰ تاریخ با فرمت استاندارد ISO 8601 تولید می‌شود. سپس با فعال‌سازی کاهش توان پردازنده (CPU Throttling) در مرورگر با ضریب ۴، تمامی سناریوهای مرتب‌سازی در شرایطی یکسان اجرا و تحلیل می‌شوند:

// Create an array of 100,000 ISO-format dates
const isoArray = [];
let currentDate = new Date(2023, 9, 1); // October 1, 2023

for (let i = 0; i < 100000; i++) {
  const year = currentDate.getFullYear();
  const month = String(currentDate.getMonth() + 1).padStart(2, '0');
  const day = String(currentDate.getDate()).padStart(2, '0');

  isoArray.push({ date: `${year}-${month}-${day}`, value: i });
  currentDate.setDate(currentDate.getDate() + 1); // advance by one day
}

// Shuffle the array to simulate unsorted input
function shuffle(array) {
  for (let i = array.length - 1; i > 0; i--) {
    const j = Math.floor(Math.random() * (i + 1));
    [array[i], array[j]] = [array[j], array[i]];
  }
}

shuffle(isoArray);

هزینه تبدیل تاریخ‌ها

در این مرحله از آزمایش، مرتب‌سازی بر اساس متد new Date() انجام می‌گیرد، به این صورت که هر تاریخ جدید به‌طور مستقیم در داخل متد sort ساخته می‌شود:

console.time('sort_with_date_conversion');

// Sorting by converting each string to a Date object on every comparison
const sortedByDate = isoArray.sort((a, b) => {
  return new Date(a.date) - new Date(b.date);
});

console.timeEnd('sort_with_date_conversion');

نتیجه در مرورگر:

sort_with_date_conversion: 1629.466796875 ms

مرتب‌سازی ۱۰۰٬۰۰۰ تاریخ تقریباً ۲ ثانیه زمان برد.

این روش اگرچه رایج است، اما هنگام پردازش مجموعه‌داده‌های بزرگ می‌تواند موجب ایجاد سربار سنگینی شود.

قدرت مرتب‌سازی واژگانی در فرمت ISO 8601

رشته‌های تاریخ با فرمت ISO 8601 ویژگی مهمی دارند: این رشته‌ها به‌صورت ذاتی قابل مرتب‌سازی واژگانی (lexicographical sorting) هستند. به همین دلیل، می‌توانیم از تبدیل آن‌ها به آبجکت‌های Date صرف‌نظر کرده و مستقیماً با خود رشته‌ها کار کنیم:

console.time('sort_by_iso_string');

// Compare strings directly — thanks to ISO 8601 format
const sorted = isoArray.sort((a, b) => 
  a.date > b.date ? 1 : -1
);

console.timeEnd('sort_by_iso_string');
console.log(sorted.slice(0, 10));

خروجی در کنسول:

sort_by_iso_string: 10.549072265625 ms
[
  { date: '2023-10-01', value: 0 },
  { date: '2023-10-02', value: 1 },
  { date: '2023-10-03', value: 2 },
  { date: '2023-10-04', value: 3 },
  { date: '2023-10-05', value: 4 },
  { date: '2023-10-06', value: 5 },
  { date: '2023-10-07', value: 6 },
  { date: '2023-10-08', value: 7 },
  { date: '2023-10-09', value: 8 },
  { date: '2023-10-10', value: 9 }
]

مدت زمان اجرای عملیات از ۱۶۰۰ میلی‌ثانیه به حدود ۱۰ میلی‌ثانیه کاهش پیدا کرد. یعنی ۱۶۰ برابر سریع‌تر!

چرا این روش سریع‌تر است؟

استفاده از new Date() در داخل متد .sort() باعث می‌شود که برای هر مقایسه، دو آبجکت جدید از نوع Date ساخته شود. زمانی که حجم داده‌ها بالا باشد (برای مثال ۱۰۰٬۰۰۰ آیتم)، این رویکرد میلیون‌ها آبجکت جدید ایجاد می‌کند و منابع سیستم را به‌شدت درگیر می‌سازد.

در مقابل، مرتب‌سازی واژگانی تنها به مقایسه ساده رشته‌ها محدود می‌شود، فرآیندی که از نظر عملکردی بسیار سبک‌تر و سریع‌تر است.

اگر تاریخ‌ها در فرمت ISO نباشند چه باید کرد؟

در بسیاری از پروژه‌ها، ممکن است تاریخ‌ها با فرمتی مانند MM/DD/YYYY ذخیره شده باشند. این نوع رشته‌ها به‌صورت واژگانی قابل مرتب‌سازی نیستند. بنابراین، باید آن‌ها را ابتدا به یک فرمت قابل مرتب‌سازی مانند ISO 8601 تبدیل کرده و سپس مرتب‌سازی کنیم.

تبدیل، سپس مرتب‌سازی

در این رویکرد، ابتدا تاریخ‌ها به فرمت قابل مرتب‌سازی تبدیل می‌شوند و سپس عملیات مرتب‌سازی انجام می‌گیرد:

console.time('sort_with_iso_conversion_first');

const sortedByISO = mdyArray
  .map((item) => { // First convert to ISO format
    const [month, day, year] = item.date.split('/');
    return { date: `${year}-${month}-${day}`, value: item.value };
  })
  .sort((a, b) => (a.date > b.date ? 1 : -1)); // then sort

console.timeEnd('sort_with_iso_conversion_first');

خروجی به‌صورت زیر خواهد بود:

sort_with_iso_conversion_first: 58.8779296875 ms

نتیجه همچنان در محدوده درک آنی توسط کاربر قرار دارد.

حفظ ساختار داده‌های اصلی

در صورتی‌که نیاز باشد داده‌های اصلی حفظ شوند (برای مثال، تاریخ‌ها همچنان در فرمت اولیه باقی بمانند)، می‌توانیم از ساختار «تاپل» استفاده کنیم. این روش امکان نگه‌داری اطلاعات اصلی را در کنار استفاده از فرمت قابل مرتب‌سازی فراهم می‌کند:

console.time('sort_and_preserve_original');

// Create tuples: [sortableDate, originalObject]
const sortedWithOriginal = mdyArray
  .map((item) => {
    const [month, day, year] = item.date.split('/');
    return [`${year}-${month}-${day}`, item]; // return the tuple items
  })
  .sort((a, b) => a[0] > b[0] ? 1 : -1) // sort based on the first item
  .map(([, item]) => item); // Return the original object

console.timeEnd('sort_and_preserve_original');

خروجی به‌صورت زیر خواهد بود:

sort_and_preserve_original: 73.733154296875 ms

همچنان در محدوده درک آنی توسط کاربر باقی می‌ماند.

اطلاعات اصلی دست‌نخورده باقی می‌مانند و عملکرد نهایی به‌قدری سریع است که کاربر تأخیری احساس نمی‌کند.

نکات کلیدی

جمع‌بندی

ما در این مقاله به بررسی چالش‌های مرتبط با مرتب سازی تاریخ در جاوااسکریپت پرداختیم و نشان دادیم که روش‌های متداول مانند استفاده مستقیم از new Date() درون متد .sort() می‌توانند به‌شدت ناکارآمد باشند، به‌ویژه هنگام کار با آرایه‌هایی با داده‌های حجیم.

با نگاهی دقیق‌تر، دریافتیم که فرمت ISO 8601 امکان مرتب‌سازی واژگانی را به‌طور ذاتی فراهم می‌کند و مقایسه مستقیم رشته‌ها در این فرمت می‌تواند عملکرد را تا بیش از ۱۶۰ برابر بهبود بخشد. همچنین برای تاریخ‌هایی با فرمت‌های غیر قابل مرتب‌سازی، تبدیل موقت آن‌ها به فرمت مناسب پیش از مرتب‌سازی، راهکاری مؤثر است.

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

در نهایت، باید توجه داشت که بهینه‌سازی فرآیند مرتب‌سازی تاریخ‌ها تنها به بهبود سرعت محدود نمی‌شود؛ بلکه می‌تواند تأثیر مستقیمی بر کیفیت تجربه کاربری، به‌ویژه در رابط‌های کاربری با داده‌های بلادرنگ یا لیست‌های طولانی، داشته باشد.