کارهای طولانی را بهینه کنید

به شما گفته شده "رشته اصلی را مسدود نکنید" و "کارهای طولانی خود را از بین ببرید"، اما انجام این کارها به چه معناست؟

جرمی واگنر
جرمی واگنر

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

  • "رشته اصلی را مسدود نکنید."
  • "کارهای طولانی خود را از بین ببرید."

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

برای اینکه بفهمید چرا بهینه سازی وظایف در جاوا اسکریپت مهم است، باید نقش وظایف و نحوه رسیدگی مرورگر به آنها را بدانید – و این با درک چیستی یک کار شروع می شود.

تکلیف چیست؟

وظیفه ، هر کار مجزایی است که مرورگر انجام می دهد. وظایف شامل کارهایی مانند رندر کردن، تجزیه HTML و CSS، اجرای کد جاوا اسکریپتی که می نویسید و موارد دیگری است که ممکن است کنترل مستقیمی روی آنها نداشته باشید. از بین همه اینها، جاوا اسکریپتی که می نویسید و در وب قرار می دهید منبع اصلی وظایف است.

تصویری از یک کار همانطور که در نمایه عملکرد ابزار DevTools کروم نشان داده شده است. این وظیفه در بالای یک پشته قرار دارد، با یک کنترل کننده رویداد کلیک، یک فراخوانی تابع، و موارد بیشتر در زیر آن. این کار همچنین شامل چند کار رندر در سمت راست است.
تصویری از یک کار که توسط یک کنترل کننده رویداد click در نمایه‌ساز عملکرد در ابزار برنامه‌نویس Chrome شروع شده است.

وظایف از چند جهت بر عملکرد تأثیر می گذارد. به عنوان مثال، هنگامی که مرورگر یک فایل جاوا اسکریپت را در حین راه اندازی دانلود می کند، وظایف را در صف می گذارد تا جاوا اسکریپت را تجزیه و کامپایل کند تا بتوان آن را اجرا کرد. بعداً در چرخه عمر صفحه، هنگامی که جاوا اسکریپت شما کارهایی مانند ایجاد تعاملات از طریق کنترل کننده رویداد، انیمیشن های مبتنی بر جاوا اسکریپت، و فعالیت های پس زمینه مانند مجموعه تجزیه و تحلیل انجام می دهد، شروع به کار می کند. همه این موارد - به استثنای وب کارگران و API های مشابه - در موضوع اصلی اتفاق می افتد.

موضوع اصلی چیست؟

موضوع اصلی جایی است که اکثر وظایف در مرورگر اجرا می شوند. به یک دلیل به آن رشته اصلی می گویند: این رشته ای است که تقریباً تمام جاوا اسکریپتی که می نویسید کار خود را انجام می دهد.

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

یک کار طولانی در نمایه ساز عملکرد DevTools کروم. قسمت مسدود کننده کار (بیشتر از 50 میلی ثانیه) با الگویی از نوارهای مورب قرمز به تصویر کشیده شده است.
یک کار طولانی همانطور که در نمایه عملکرد کروم نشان داده شده است. کارهای طولانی با یک مثلث قرمز در گوشه کار نشان داده می شوند که قسمت مسدود کننده کار با الگویی از نوارهای قرمز مورب پر شده است.

شما باید وظایف را از هم جدا کنید . این به این معنی است که یک کار طولانی را انجام دهید و آن را به کارهای کوچکتر تقسیم کنید که زمان کمتری برای اجرای جداگانه نیاز دارد.

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

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

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

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

با این حال، مشکل این است که توصیه "کارهای طولانی خود را از بین ببرید" و "رشته اصلی را مسدود نکنید" به اندازه کافی مشخص نیست، مگر اینکه از قبل بدانید که چگونه این کارها را انجام دهید. این چیزی است که این راهنما توضیح خواهد داد.

استراتژی های مدیریت وظیفه

یک توصیه رایج در معماری نرم افزار این است که کار خود را به عملکردهای کوچکتر تقسیم کنید. این به شما مزایای خوانایی بهتر کد و قابلیت نگهداری پروژه را می دهد. این همچنین نوشتن تست ها را آسان تر می کند.

function saveSettings () {
  validateForm();
  showSpinner();
  saveToDatabase();
  updateUI();
  sendAnalytics();
}

در این مثال، تابعی به نام saveSettings() وجود دارد که پنج تابع را برای انجام کار فراخوانی می‌کند، مانند اعتبارسنجی یک فرم، نشان دادن اسپینر، ارسال داده و غیره. از نظر مفهومی، این به خوبی طراحی شده است. اگر نیاز به اشکال زدایی یکی از این توابع دارید، می توانید درخت پروژه را طی کنید تا بفهمید هر تابع چه کاری انجام می دهد.

با این حال، مشکل این است که جاوا اسکریپت هر یک از این توابع را به عنوان وظایف جداگانه اجرا نمی کند زیرا آنها در تابع saveSettings() اجرا می شوند. این بدان معنی است که هر پنج تابع به عنوان یک وظیفه واحد اجرا می شوند.

عملکرد saveSettings همانطور که در نمایه‌ساز عملکرد Chrome نشان داده شده است. در حالی که تابع سطح بالا پنج تابع دیگر را فراخوانی می کند، تمام کارها در یک کار طولانی انجام می شود که رشته اصلی را مسدود می کند.
یک تابع saveSettings() که پنج تابع را فراخوانی می کند. این کار به عنوان بخشی از یک کار طولانی مدت یکپارچه اجرا می شود.

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

به تعویق انداختن دستی اجرای کد

یکی از روش هایی که توسعه دهندگان برای تقسیم وظایف به کارهای کوچکتر استفاده کرده اند، شامل setTimeout() است. با این تکنیک، تابع را به setTimeout() منتقل می کنید. این کار اجرای تماس برگشتی را به یک کار جداگانه به تعویق می‌اندازد، حتی اگر زمان 0 را مشخص کنید.

function saveSettings () {
  // Do critical work that is user-visible:
  validateForm();
  showSpinner();
  updateUI();

  // Defer work that isn't user-visible to a separate task:
  setTimeout(() => {
    saveToDatabase();
    sendAnalytics();
  }, 0);
}

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

function processData () {
  for (const item of largeDataArray) {
    // Process the individual item here.
  }
}

استفاده از setTimeout() در اینجا مشکل ساز است، زیرا ارگونومی آن اجرای آن را دشوار می کند، و کل آرایه داده ممکن است زمان بسیار زیادی برای پردازش نیاز داشته باشد، حتی اگر هر آیتم را بتوان خیلی سریع پردازش کرد. همه چیز اضافه می شود و setTimeout() ابزار مناسبی برای این کار نیست – حداقل در صورت استفاده از این روش.

علاوه بر setTimeout() ، چند API دیگر وجود دارد که به شما امکان می دهد اجرای کد را به یک کار بعدی موکول کنید. یکی شامل استفاده از postMessage() برای زمان‌بندی سریع‌تر است . همچنین می‌توانید با استفاده از requestIdleCallback() کار را جدا کنید —اما مراقب باشید!— requestIdleCallback() وظایف را با کمترین اولویت ممکن و فقط در زمان بیکاری مرورگر زمان‌بندی می‌کند. هنگامی که رشته اصلی شلوغ است، کارهای برنامه ریزی شده با requestIdleCallback() ممکن است هرگز اجرا نشوند.

از async / await برای ایجاد نقاط بازده استفاده کنید

عبارتی که در ادامه این راهنما خواهید دید، «تسلیم به موضوع اصلی» است – اما این به چه معناست؟ چرا باید این کار را انجام دهید؟ چه زمانی باید آن را انجام دهید؟

هنگامی که وظایف تقسیم می شوند، سایر وظایف را می توان با طرح اولویت بندی داخلی مرورگر اولویت بندی کرد. یکی از راه‌های تسلیم به رشته اصلی شامل استفاده از ترکیبی از یک Promise است که با فراخوانی به setTimeout() حل می‌شود:

function yieldToMain () {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

در تابع saveSettings() ، اگر بعد از هر فراخوانی تابع await تابع yieldToMain() باشید، می‌توانید پس از هر بیت کار به رشته اصلی تسلیم شوید:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread:
    await yieldToMain();
  }
}

نتیجه این است که وظیفه زمانی یکپارچه اکنون به وظایف جداگانه تقسیم می شود.

همان تابع saveSettings که در نمایه‌ساز عملکرد Chrome نشان داده شده است، فقط با تسلیم. نتیجه این است که وظیفه زمانی یکپارچه اکنون به پنج وظیفه جداگانه تقسیم می شود - یکی برای هر تابع.
تابع saveSettings() اکنون توابع فرزند خود را به عنوان وظایف جداگانه اجرا می کند.

مزیت استفاده از رویکرد مبتنی بر وعده برای بازده به جای استفاده دستی از setTimeout() ارگونومی بهتر است. نقاط بازده بیانی می شوند و بنابراین نوشتن، خواندن و درک آن آسان تر است.

محصول فقط در صورت لزوم

اگر تعدادی کار دارید، اما تنها در صورتی که کاربر سعی کند با صفحه تعامل داشته باشد، می‌خواهید تسلیم شوید، چه؟ این همان چیزی است که isInputPending() برای آن ساخته شده است.

isInputPending() تابعی است که می توانید آن را در هر زمان اجرا کنید تا مشخص کنید آیا کاربر در حال تلاش برای تعامل با یک عنصر صفحه است یا خیر: فراخوانی به isInputPending() true را برمی گرداند. در غیر این صورت false برمی گرداند.

فرض کنید یک صف از وظایفی دارید که باید اجرا کنید، اما نمی‌خواهید مانع هیچ ورودی شوید. این کد - که از هم isInputPending() و هم تابع سفارشی yieldToMain() ما استفاده می کند - تضمین می کند که در زمانی که کاربر سعی می کند با صفحه تعامل داشته باشد، ورودی به تأخیر نیفتد:

async function saveSettings () {
  // A task queue of functions
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ];
  
  while (tasks.length > 0) {
    // Yield to a pending user input:
    if (navigator.scheduling.isInputPending()) {
      // There's a pending user input. Yield here:
      await yieldToMain();
    } else {
      // Shift the task out of the queue:
      const task = tasks.shift();

      // Run the task:
      task();
    }
  }
}

در حالی که saveSettings() اجرا می شود، روی وظایف موجود در صف حلقه می زند. اگر isInputPending() در طول حلقه true را برگرداند، saveSettings() yieldToMain() را فراخوانی می کند تا بتوان ورودی کاربر را مدیریت کرد. در غیر این صورت، کار بعدی را از جلوی صف جابجا می کند و آن را به طور مداوم اجرا می کند. این کار را تا زمانی انجام می دهد که هیچ کار دیگری باقی نماند.

تصویری از عملکرد saveSettings که در نمایه‌ساز عملکرد Chrome اجرا می‌شود. وظیفه به‌دست‌آمده، رشته اصلی را مسدود می‌کند تا زمانی که isInputPending true را برگرداند، در این مرحله، وظیفه به رشته اصلی تسلیم می‌شود.
saveSettings() یک صف کار را برای پنج کار اجرا می کند، اما کاربر برای باز کردن یک منو در حالی که دومین مورد کار در حال اجرا بود کلیک کرده است. isInputPending() به رشته اصلی تسلیم می شود تا تعامل را مدیریت کند و اجرای بقیه وظایف را از سر بگیرد.

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

راه دیگری برای استفاده از isInputPending() - به خصوص اگر نگران ارائه یک بازگشت به عقب برای مرورگرهایی هستید که از آن پشتیبانی نمی کنند - استفاده از یک رویکرد مبتنی بر زمان در ارتباط با عملگر زنجیره ای اختیاری است:

async function saveSettings () {
  // A task queue of functions
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ];
  
  let deadline = performance.now() + 50;

  while (tasks.length > 0) {
    // Optional chaining operator used here helps to avoid
    // errors in browsers that don't support `isInputPending`:
    if (navigator.scheduling?.isInputPending() || performance.now() >= deadline) {
      // There's a pending user input, or the
      // deadline has been reached. Yield here:
      await yieldToMain();

      // Extend the deadline:
      deadline = performance.now() + 50;

      // Stop the execution of the current loop and
      // move onto the next iteration:
      continue;
    }

    // Shift the task out of the queue:
    const task = tasks.shift();

    // Run the task:
    task();
  }
}

با این رویکرد، با استفاده از یک رویکرد مبتنی بر زمان که از یک ضرب‌الاجل استفاده می‌کند (و تنظیم می‌کند) برای مرورگرهایی که isInputPending() پشتیبانی نمی‌کنند، یک بازگشت مجدد دریافت می‌کنید تا کار در صورت لزوم، چه با تسلیم شدن به ورودی کاربر، شکسته شود. یا در یک مقطع زمانی خاص

شکاف در APIهای فعلی

API های ذکر شده تا کنون می توانند به شما کمک کنند تا وظایف را تقسیم کنید، اما آنها یک جنبه منفی دارند: وقتی با به تعویق انداختن کد برای اجرای یک کار بعدی، به رشته اصلی تسلیم می شوید، آن کد به انتهای صف کار اضافه می شود.

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

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

یک API زمانبندی اختصاصی

API زمانبند در حال حاضر تابع postTask() را ارائه می دهد که در زمان نگارش در مرورگرهای Chromium و در فایرفاکس پشت پرچم موجود است. postTask() امکان زمان‌بندی دقیق‌تری از وظایف را فراهم می‌کند و یکی از راه‌های کمک به مرورگر برای اولویت‌بندی کارها است تا وظایف با اولویت پایین به رشته اصلی تسلیم شوند. postTask() از وعده‌ها استفاده می‌کند و یک تنظیم priority را می‌پذیرد.

API postTask() دارای سه اولویت است که می توانید از آنها استفاده کنید:

  • 'background' برای وظایف با کمترین اولویت.
  • 'user-visible' برای وظایف با اولویت متوسط. اگر priority تنظیم نشده باشد، این پیش‌فرض است.
  • 'user-blocking' برای کارهای حیاتی که باید با اولویت بالا اجرا شوند.

کد زیر را به عنوان مثال در نظر بگیرید، که در آن API postTask() برای اجرای سه وظیفه با بالاترین اولویت ممکن و دو وظیفه باقی مانده در کمترین اولویت ممکن استفاده می شود.

function saveSettings () {
  // Validate the form at high priority
  scheduler.postTask(validateForm, {priority: 'user-blocking'});

  // Show the spinner at high priority:
  scheduler.postTask(showSpinner, {priority: 'user-blocking'});

  // Update the database in the background:
  scheduler.postTask(saveToDatabase, {priority: 'background'});

  // Update the user interface at high priority:
  scheduler.postTask(updateUI, {priority: 'user-blocking'});

  // Send analytics data in the background:
  scheduler.postTask(sendAnalytics, {priority: 'background'});
};

در اینجا، اولویت وظایف به گونه‌ای برنامه‌ریزی می‌شود که وظایف اولویت‌بندی شده مرورگر - مانند تعاملات کاربر - بتواند به خوبی انجام شود.

عملکرد saveSettings همانطور که در نمایه‌ساز عملکرد کروم نشان داده شده است، اما از postTask استفاده می‌کند. postTask هر تابعی را که saveSettings اجرا می‌شود تقسیم می‌کند و آنها را به گونه‌ای اولویت‌بندی می‌کند که تعامل کاربر فرصتی برای اجرا بدون مسدود شدن داشته باشد.
هنگامی که saveSettings() اجرا می شود، تابع با استفاده از postTask() توابع جداگانه را زمان بندی می کند. کارهای حساس رو به روی کاربر با اولویت بالا برنامه ریزی شده است، در حالی که کارهایی که کاربر از آن اطلاعی ندارد در پس زمینه برنامه ریزی شده است. این اجازه می دهد تا تعاملات کاربر با سرعت بیشتری اجرا شود، زیرا کار هم شکسته شده و هم به طور مناسب اولویت بندی می شود.

این یک مثال ساده از نحوه استفاده از postTask() است. امکان نمونه سازی اشیاء مختلف TaskController وجود دارد که می توانند اولویت ها را بین وظایف به اشتراک بگذارند، از جمله توانایی تغییر اولویت ها برای نمونه های مختلف TaskController در صورت نیاز.

بازده داخلی با ادامه از طریق scheduler.yield

یکی از بخش‌های پیشنهادی API زمان‌بند scheduler.yield است، یک API که به‌طور خاص برای تسلیم شدن به رشته اصلی در مرورگر طراحی شده است که در حال حاضر برای آزمایش به‌عنوان آزمایشی اصلی در دسترس است . استفاده از آن شبیه تابع yieldToMain() است که قبلا در این مقاله نشان داده شد:

async function saveSettings () {
  // Create an array of functions to run:
  const tasks = [
    validateForm,
    showSpinner,
    saveToDatabase,
    updateUI,
    sendAnalytics
  ]

  // Loop over the tasks:
  while (tasks.length > 0) {
    // Shift the first task off the tasks array:
    const task = tasks.shift();

    // Run the task:
    task();

    // Yield to the main thread with the scheduler
    // API's own yielding mechanism:
    await scheduler.yield();
  }
}

توجه داشته باشید که کد بالا تا حد زیادی آشنا است، اما به جای استفاده از yieldToMain() ، به جای آن، await scheduler.yield() .

سه نمودار که وظایف را بدون تسلیم، تسلیم و با تسلیم و ادامه نشان می دهد. بدون تسلیم، وظایف طولانی وجود دارد. با تسلیم، وظایف بیشتری وجود دارد که کوتاه‌تر هستند، اما ممکن است توسط سایر وظایف نامرتبط قطع شوند. با تسلیم و ادامه، وظایف بیشتری وجود دارد که کوتاهتر هستند، اما ترتیب اجرای آنها حفظ می شود.
تجسم اجرای کار بدون تسلیم، با تسلیم، و با تسلیم و ادامه. هنگامی که scheduler.yield() استفاده می شود، اجرای کار از جایی که متوقف شد، حتی پس از نقطه تسلیم ادامه می یابد.

مزیت scheduler.yield() continuation است، به این معنی که اگر در وسط مجموعه ای از وظایف تسلیم شوید، سایر وظایف زمان بندی شده به همان ترتیب پس از نقطه بازده ادامه خواهند داشت. با این کار کدهای اسکریپت های شخص ثالث از غصب نظم اجرای کد شما جلوگیری می کند.

نتیجه

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

  • تسلیم موضوع اصلی برای کارهای حیاتی و مورد نظر کاربر شوید.
  • از isInputPending() برای تسلیم شدن به رشته اصلی زمانی که کاربر سعی در تعامل با صفحه دارد استفاده کنید.
  • با postTask() وظایف را اولویت بندی کنید.
  • در نهایت، تا حد امکان در عملکردهای خود کمتر کار کنید.

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

تشکر ویژه از فیلیپ والتون برای بررسی فنی این مقاله.

تصویر قهرمان برگرفته از سایت Unsplash توسط امیرعلی میرهاشمیان .