بهینه‌سازی INP (Interaction to Next Paint): راهنمای عملی بهبود تعامل‌پذیری وب در ۲۰۲۶

راهنمای عملی بهینه‌سازی INP با تکنیک‌های مدرن شامل scheduler.yield، Web Workers، useTransition و CSS Containment. از سه فاز تعامل تا مطالعات موردی Trendyol، redBus و Hotstar با نتایج واقعی.

بهینه‌سازی INP (Interaction to Next Paint): راهنمای عملی بهبود تعامل‌پذیری وب در ۲۰۲۶

دست‌تون رو بذارید رو قلب‌تون و صادقانه بگید: چند بار با یه وب‌سایت کار کردید که وقتی روی یه دکمه کلیک کردید، انگار سایت رفته بود تو کما؟ هیچ اتفاقی نمیفته، شما هم دوباره کلیک می‌کنید، باز هیچی. خب، دقیقاً همین مشکلیه که INP (Interaction to Next Paint) قراره اندازه بگیره.

این معیار از مارس ۲۰۲۴ جایگزین FID (First Input Delay) به عنوان یکی از Core Web Vitals گوگل شده و صادقانه بگم، تو سال ۲۰۲۶ نادیده گرفتنش دیگه واقعاً جایی نداره.

FID فقط تأخیر اولین تعامل کاربر رو اندازه می‌گرفت. یعنی چی؟ یعنی اگه اولین کلیک کاربر سریع پردازش می‌شد ولی بقیه تعاملات فاجعه بود، FID خیلی شیک بهتون نمره خوب می‌داد. INP اومد و این داستان رو عوض کرد: تمام تعاملات کاربر رو در طول عمر صفحه بررسی می‌کنه — کلیک‌ها، ضربه‌های انگشت روی موبایل، فشردن کلیدهای کیبورد — و بدترین‌شون رو (با یه تعدیل آماری) به عنوان امتیاز INP صفحه گزارش میده.

خب، تو این مقاله قراره از صفر تا صد INP رو با هم بررسی کنیم. از اینکه دقیقاً چطور کار می‌کنه، تا تکنیک‌های عملی بهینه‌سازی با کدهای واقعی که بتونید مستقیم تو پروژه‌هاتون ازشون استفاده کنید. یه خبر هیجان‌انگیز هم دارم: در سال ۲۰۲۶، سافاری هم بالاخره قراره LCP و INP رو پشتیبانی کنه. این یعنی دیگه این معیارها فقط مختص کروم و فایرفاکس نیستن و واقعاً جهانی میشن.

درک عمیق INP: سه فاز تعامل

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

فاز ۱: تأخیر ورودی (Input Delay)

این فاز از لحظه‌ای شروع میشه که کاربر کلیک می‌کنه (یا دکمه‌ای رو فشار میده) تا لحظه‌ای که مرورگر شروع به اجرای event handler مربوطه می‌کنه. حالا چرا تأخیر داره؟ چون ممکنه main thread مرورگر مشغول کار دیگه‌ای باشه — مثلاً داره یه اسکریپت سنگین اجرا می‌کنه، یا یه تایمر در حال اجراست، یا یه task دیگه تو صف منتظره.

یه تشبیه ساده: تصور کنید تو یه رستوران شلوغ نشستید و دست بلند می‌کنید ولی گارسون داره میز بغلی رو سرویس میده. اون فاصله بین بلند کردن دست شما و رسیدن گارسون، دقیقاً همون Input Delay هست.

فاز ۲: زمان پردازش (Processing Time)

این فاز شامل اجرای تمام event handler‌های مربوط به اون تعامله. مثلاً وقتی روی یه دکمه کلیک می‌کنید، ممکنه چندین event listener براش تعریف شده باشه — pointerdown، mousedown، pointerup، mouseup، click — و همه‌شون باید اجرا بشن. اگه این handler‌ها کار سنگینی انجام بدن (محاسبات پیچیده، درخواست‌های سنکرون، دستکاری گسترده DOM)، این فاز طولانی میشه.

فاز ۳: تأخیر ارائه (Presentation Delay)

بعد از اینکه event handler‌ها کارشون تموم شد، مرورگر باید تغییرات بصری رو به کاربر نشون بده. این شامل محاسبه استایل‌ها (Style)، چیدمان (Layout)، نقاشی (Paint) و ترکیب لایه‌ها (Composite) میشه. اگه تغییرات DOM بزرگ باشن، CSS‌های پیچیده‌ای درگیر باشن، یا DOM صفحه خیلی حجیم باشه، این مرحله هم زمان‌بر میشه.

INP = Input Delay + Processing Time + Presentation Delay

نکته مهم اینه که برای بهینه‌سازی مؤثر، باید بدونید کدوم فاز بیشترین سهم رو تو تأخیر داره. ابزارهایی مثل Chrome DevTools این تفکیک رو بهتون نشون میدن — که جلوتر بهشون می‌پردازیم.

آستانه‌های گوگل و یه آمار جالب

گوگل سه سطح برای INP تعریف کرده:

  • خوب (Good): ≤۲۰۰ میلی‌ثانیه — کاربر احساس می‌کنه سایت فوری جواب میده
  • نیاز به بهبود (Needs Improvement): بین ۲۰۰ تا ۵۰۰ میلی‌ثانیه — قابل توجه ولی هنوز قابل تحمل
  • ضعیف (Poor): بیشتر از ۵۰۰ میلی‌ثانیه — کاربر واضحاً احساس کندی و لگ می‌کنه

حالا یه آمار که شاید غافلگیرتون کنه: طبق داده‌های CrUX (Chrome User Experience Report)، حدود ۹۶.۸٪ سایت‌ها روی دسکتاپ INP خوبی دارن. ولی روی موبایل فقط ۶۴.۹٪ سایت‌ها از این آستانه رد میشن. این اختلاف بزرگه و دلایلش هم مشخصه:

  • پردازنده‌های موبایل ضعیف‌ترن — به خصوص گوشی‌های میان‌رده و پایین‌رده
  • حافظه رم کمتره و garbage collection بیشتر اتفاق میفته
  • صفحه لمسی تعاملات بیشتری ایجاد می‌کنه (scroll، tap، swipe)
  • شبکه‌های موبایل latency بالاتری دارن که روی بارگذاری اسکریپت‌ها تأثیر می‌ذاره

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

مطالعات موردی واقعی: وقتی INP خوب میشه، درآمد هم خوب میشه

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

Trendyol — غول تجارت الکترونیک ترکیه

Trendyol موفق شد INP صفحاتش رو ۵۰٪ کاهش بده. نتیجه؟ ۱٪ افزایش در نرخ کلیک (CTR). شاید ۱٪ کم به نظر برسه، ولی وقتی میلیون‌ها کاربر دارید، این ۱٪ ترجمه میشه به میلیون‌ها تومان درآمد بیشتر. جالب اینجاست که تنها کاری که کردن بهبود پاسخ‌گویی صفحه بود — نه تغییر طراحی، نه تغییر قیمت. فقط سریع‌تر کردن تعاملات.

redBus — پلتفرم رزرو بلیط هند

redBus با بهبود INP تونست ۷٪ افزایش فروش رو تجربه کنه. منطقش ساده‌ست: وقتی کاربر روی دکمه «رزرو» کلیک می‌کنه و سریع پاسخ می‌گیره، احتمال اینکه فرآیند خرید رو کامل کنه خیلی بیشتره تا وقتی که بعد از کلیک، نیم ثانیه هیچ اتفاقی نیفته.

Hotstar — سرویس استریمینگ

این یکی واقعاً چشم‌گیره. Hotstar با ۶۱٪ کاهش INP تونست ۱۰۰٪ افزایش در بازدید کارت‌ها (card views) رو ببینه. بله، درست خوندید — دو برابر! وقتی کارت‌های محتوا سریع به تعامل کاربر پاسخ بدن، کاربر بیشتر کاوش می‌کنه و محتوای بیشتری مصرف می‌کنه.

پیام این نتایج روشنه: INP فقط یه معیار فنی نیست. مستقیماً روی درآمد و تجربه کاربری تأثیر می‌ذاره.

تکنیک ۱: شکستن تسک‌های طولانی (Long Tasks)

بزرگ‌ترین دشمن INP، Long Tasks هستن — تسک‌هایی که بیشتر از ۵۰ میلی‌ثانیه main thread رو اشغال می‌کنن. وقتی main thread مشغول یه تسک طولانیه، هر تعاملی که کاربر انجام بده باید صبر کنه تا اون تسک تموم بشه. و این صبر کردن، دقیقاً همون Input Delay هست.

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

روش کلاسیک: setTimeout

ساده‌ترین روش استفاده از setTimeout با تأخیر صفره:

// Before: One long blocking task
function processLargeArray(items) {
  items.forEach(item => {
    heavyComputation(item);
  });
}

// After: Broken into yielding chunks
function processLargeArrayOptimized(items) {
  const CHUNK_SIZE = 50;
  let index = 0;

  function processChunk() {
    const end = Math.min(index + CHUNK_SIZE, items.length);

    for (let i = index; i < end; i++) {
      heavyComputation(items[i]);
    }

    index = end;

    if (index < items.length) {
      // Yield to the main thread, then continue
      setTimeout(processChunk, 0);
    }
  }

  processChunk();
}

این روش کار می‌کنه ولی یه مشکلاتی داره: setTimeout(..., 0) واقعاً صفر میلی‌ثانیه نیست. مرورگرها معمولاً حداقل ۴ میلی‌ثانیه تأخیر اعمال می‌کنن و اولویت‌بندی تسک‌ها هم مشخص نیست. یعنی ممکنه ادامه کار شما خیلی دیر اجرا بشه.

روش مدرن: scheduler.yield()

خب، اینجا یه API جدید و خیلی باحال وارد بازی میشه. scheduler.yield() دقیقاً برای همین مشکل طراحی شده. وقتی ازش استفاده می‌کنید، مرورگر اول به تسک‌های با اولویت بالاتر (مثل تعاملات کاربر) رسیدگی می‌کنه و بعد ادامه کار شما رو از سر می‌گیره:

async function processItems(items) {
  for (let i = 0; i < items.length; i++) {
    heavyComputation(items[i]);

    // Every 50 items, yield to let browser handle interactions
    if (i % 50 === 0 && i > 0) {
      await scheduler.yield();
    }
  }
}

// Feature detection for backward compatibility
async function yieldToMain() {
  if ('scheduler' in window && 'yield' in scheduler) {
    await scheduler.yield();
  } else {
    // Fallback to setTimeout
    await new Promise(resolve => setTimeout(resolve, 0));
  }
}

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

روش پیشرفته: scheduler.postTask()

اگه کنترل بیشتری روی اولویت‌بندی می‌خواید، scheduler.postTask() هست:

// High priority - for user-visible updates
scheduler.postTask(() => {
  updateUIElement();
}, { priority: 'user-blocking' });

// Normal priority - for important but not urgent work
scheduler.postTask(() => {
  fetchAndCacheData();
}, { priority: 'user-visible' });

// Low priority - for background tasks
scheduler.postTask(() => {
  sendAnalytics();
  preloadImages();
}, { priority: 'background' });

// Practical example: Processing form submission
async function handleFormSubmit(formData) {
  // Immediately show loading state (user-blocking)
  await scheduler.postTask(() => {
    showLoadingSpinner();
  }, { priority: 'user-blocking' });

  // Validate and submit (user-visible)
  await scheduler.postTask(async () => {
    const result = await submitToServer(formData);
    updateUI(result);
  }, { priority: 'user-visible' });

  // Track analytics (background)
  scheduler.postTask(() => {
    trackFormSubmission(formData);
  }, { priority: 'background' });
}

با این API مطمئن میشید کارهای مهم برای کاربر اول انجام میشن و کارهای پس‌زمینه‌ای مثل analytics و preloading بعداً پردازش میشن. خیلی تمیز و منطقی.

تکنیک ۲: بهینه‌سازی Event Handlerها

راستش رو بخواید، خیلی وقت‌ها مشکل INP از خود event handler‌هاییه که ما نوشتیم. بیاید چند تکنیک کلیدی رو ببینیم.

Debouncing و Throttling

اگه event handler شما روی رویدادهایی مثل input، scroll یا resize هست، حتماً از debounce یا throttle استفاده کنید. اگه نمی‌کنید... خب، باید بکنید:

// Debounce: Wait until user stops typing
function debounce(fn, delay) {
  let timeoutId;
  return function (...args) {
    clearTimeout(timeoutId);
    timeoutId = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Throttle: Run at most once every X ms
function throttle(fn, limit) {
  let inThrottle = false;
  return function (...args) {
    if (!inThrottle) {
      fn.apply(this, args);
      inThrottle = true;
      setTimeout(() => inThrottle = false, limit);
    }
  };
}

// Usage: Search suggestions
const searchInput = document.getElementById('search');
searchInput.addEventListener('input', debounce((e) => {
  fetchSuggestions(e.target.value);
}, 300));

// Usage: Scroll tracking
window.addEventListener('scroll', throttle(() => {
  updateScrollProgress();
}, 100));

دستکاری بهینه DOM

هر بار که DOM رو تغییر میدید، مرورگر ممکنه مجبور بشه Layout رو دوباره محاسبه کنه. و اگه بین تغییرات، مقادیر layout رو هم بخونید، وارد چیزی به نام Layout Thrashing میشید — که صادقانه بگم، یکی از رایج‌ترین دلایل INP بد هست:

// BAD: Layout thrashing - forces multiple layout recalculations
function resizeElements(elements) {
  elements.forEach(el => {
    const height = el.offsetHeight; // Read (forces layout)
    el.style.height = (height * 2) + 'px'; // Write (invalidates layout)
    // Next iteration reads again, forcing another layout!
  });
}

// GOOD: Batch reads, then batch writes
function resizeElementsOptimized(elements) {
  // First pass: read all values
  const heights = elements.map(el => el.offsetHeight);

  // Second pass: write all values
  elements.forEach((el, i) => {
    el.style.height = (heights[i] * 2) + 'px';
  });
}

// EVEN BETTER: Use requestAnimationFrame for visual updates
function updateVisuals(elements) {
  const heights = elements.map(el => el.offsetHeight);

  requestAnimationFrame(() => {
    elements.forEach((el, i) => {
      el.style.height = (heights[i] * 2) + 'px';
    });
  });
}

استفاده از DocumentFragment

وقتی می‌خواید تعداد زیادی عنصر به DOM اضافه کنید، از DocumentFragment استفاده کنید تا فقط یک بار reflow اتفاق بیفته. تفاوتش قابل‌توجهه:

// BAD: Multiple DOM insertions trigger multiple reflows
function addItems(items) {
  const list = document.getElementById('list');
  items.forEach(item => {
    const li = document.createElement('li');
    li.textContent = item.name;
    list.appendChild(li); // Each append can trigger reflow
  });
}

// GOOD: Single DOM insertion with DocumentFragment
function addItemsOptimized(items) {
  const list = document.getElementById('list');
  const fragment = document.createDocumentFragment();

  items.forEach(item => {
    const li = document.createElement('li');
    li.textContent = item.name;
    fragment.appendChild(li); // No reflow here
  });

  list.appendChild(fragment); // Single reflow
}

تکنیک ۳: Web Workers برای محاسبات سنگین

بعضی کارها ذاتاً سنگینن و هیچ‌کاری‌شون نمیشه کرد — مرتب‌سازی آرایه‌های بزرگ، پردازش تصویر، محاسبات رمزنگاری و از این قبیل. اینجاست که Web Workers وارد میشن و نجاتتون میدن. Web Worker‌ها کد جاوااسکریپت رو تو یه thread جدا اجرا می‌کنن، بدون اینکه main thread رو بلاک کنن.

// main.js - Main thread
const worker = new Worker('/workers/data-processor.js');

// Send data to worker
function processDataInBackground(data) {
  return new Promise((resolve, reject) => {
    worker.onmessage = (e) => {
      if (e.data.error) {
        reject(new Error(e.data.error));
      } else {
        resolve(e.data.result);
      }
    };

    worker.postMessage({ type: 'PROCESS', payload: data });
  });
}

// Usage in event handler
async function handleFilterChange(filterCriteria) {
  // Show loading state immediately
  showLoadingIndicator();

  // Heavy computation happens off main thread
  const filteredResults = await processDataInBackground({
    items: allProducts,
    filters: filterCriteria
  });

  // Only DOM update happens on main thread
  renderResults(filteredResults);
  hideLoadingIndicator();
}
// workers/data-processor.js - Worker thread
self.onmessage = function(e) {
  const { type, payload } = e.data;

  switch (type) {
    case 'PROCESS':
      try {
        const result = filterAndSort(payload.items, payload.filters);
        self.postMessage({ result });
      } catch (error) {
        self.postMessage({ error: error.message });
      }
      break;
  }
};

function filterAndSort(items, filters) {
  // Heavy computation - runs in separate thread
  let result = items.filter(item => {
    return Object.entries(filters).every(([key, value]) => {
      return item[key] === value || !value;
    });
  });

  result.sort((a, b) => a.price - b.price);

  return result;
}

یه نکته مهم: Web Worker‌ها به DOM دسترسی ندارن. پس فقط محاسبات و پردازش داده رو بفرستید به Worker و آپدیت UI رو تو main thread انجام بدید. این تفکیک در واقع الگوی خوبی هم هست و کدتون رو تمیزتر نگه می‌داره.

تکنیک ۴: بهینه‌سازی‌های مخصوص React و فریمورک‌ها

اگه با React کار می‌کنید (که خب، احتمالاً خیلی‌هاتون کار می‌کنید!)، یه سری ابزار قدرتمند در اختیار دارید. React 19.2 هم ویژگی‌های جدیدی مخصوص بهینه‌سازی INP آورده.

useTransition: جدا کردن آپدیت‌های فوری از غیرفوری

useTransition بهتون اجازه میده بعضی آپدیت‌های state رو به عنوان «غیرفوری» علامت بزنید. مرورگر اول آپدیت‌های فوری رو انجام میده و بعد، وقتی فرصت داشت، آپدیت‌های غیرفوری رو پردازش می‌کنه:

import { useState, useTransition } from 'react';

function SearchPage() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [isPending, startTransition] = useTransition();

  function handleSearch(e) {
    const value = e.target.value;

    // Urgent: Update the input field immediately
    setQuery(value);

    // Non-urgent: Update search results can wait
    startTransition(() => {
      const filtered = filterProducts(value); // expensive operation
      setResults(filtered);
    });
  }

  return (
    <div>
      <input
        value={query}
        onChange={handleSearch}
        placeholder="جستجو..."
      />
      {isPending && <div className="spinner" />}
      <ResultsList results={results} />
    </div>
  );
}

ببینید چه اتفاقی میفته: وقتی کاربر تایپ می‌کنه، input فوراً آپدیت میشه و احساس سریع بودن میده. ولی فیلتر کردن نتایج جستجو — که ممکنه سنگین باشه — به عنوان transition انجام میشه و main thread رو برای تعاملات بعدی آزاد نگه می‌داره. خیلی هوشمندانه‌ست.

useDeferredValue: به تأخیر انداختن مقادیر سنگین

useDeferredValue مکمل useTransition هست. وقتی یه مقدار خیلی سریع تغییر می‌کنه و render کردنش سنگینه، می‌تونید نسخه deferred اون مقدار رو استفاده کنید:

import { useState, useDeferredValue, useMemo } from 'react';

function ProductList({ products }) {
  const [filter, setFilter] = useState('');
  const deferredFilter = useDeferredValue(filter);

  // Only re-compute when deferred value changes
  const filteredProducts = useMemo(() => {
    return products.filter(p =>
      p.name.toLowerCase().includes(deferredFilter.toLowerCase())
    );
  }, [products, deferredFilter]);

  const isStale = filter !== deferredFilter;

  return (
    <div>
      <input
        value={filter}
        onChange={e => setFilter(e.target.value)}
        placeholder="فیلتر محصولات..."
      />
      <div style={{ opacity: isStale ? 0.7 : 1 }}>
        {filteredProducts.map(p => (
          <ProductCard key={p.id} product={p} />
        ))}
      </div>
    </div>
  );
}

یه نکته ظریف اینجا هست: وقتی isStale هست (یعنی مقدار واقعی از مقدار deferred جلوتره)، opacity رو کم می‌کنیم تا کاربر بفهمه نتایج در حال آپدیت شدنه. یه الگوی UX ساده ولی خیلی مؤثر.

Virtual Scrolling برای لیست‌های بزرگ

اگه لیستی با صدها یا هزاران آیتم دارید، render کردن همه‌شون همزمان هم INP رو خراب می‌کنه هم حافظه رو می‌خوره. Virtual scrolling فقط آیتم‌هایی رو render می‌کنه که تو viewport قابل مشاهده هستن:

import { useVirtualizer } from '@tanstack/react-virtual';
import { useRef } from 'react';

function VirtualProductList({ products }) {
  const parentRef = useRef(null);

  const virtualizer = useVirtualizer({
    count: products.length,
    getScrollElement: () => parentRef.current,
    estimateSize: () => 80, // estimated row height in px
    overscan: 5, // render 5 extra items above/below viewport
  });

  return (
    <div
      ref={parentRef}
      style={{ height: '600px', overflow: 'auto' }}
    >
      <div
        style={{
          height: `${virtualizer.getTotalSize()}px`,
          width: '100%',
          position: 'relative',
        }}
      >
        {virtualizer.getVirtualItems().map(virtualRow => (
          <div
            key={virtualRow.key}
            style={{
              position: 'absolute',
              top: 0,
              left: 0,
              width: '100%',
              height: `${virtualRow.size}px`,
              transform: `translateY(${virtualRow.start}px)`,
            }}
          >
            <ProductCard product={products[virtualRow.index]} />
          </div>
        ))}
      </div>
    </div>
  );
}

با virtual scrolling، حتی اگه ۱۰,۰۰۰ محصول داشته باشید، فقط مثلاً ۲۰ تاشون در هر لحظه render میشن. تأثیرش روی INP و مصرف حافظه واقعاً فوق‌العاده‌ست.

تکنیک ۵: کاهش حجم DOM و CSS Containment

یه عاملی هست که خیلی‌ها بهش توجه نمی‌کنن (و من خودم هم یه زمانی بهش بی‌توجه بودم): اندازه DOM. هرچی DOM بزرگ‌تر باشه، محاسبات Style و Layout سنگین‌تر میشن و Presentation Delay بیشتر میشه.

توصیه‌ها برای کاهش DOM

  • از عناصر wrapper غیرضروری اجتناب کنید — از Fragment در React استفاده کنید
  • محتوای خارج از viewport رو lazy load کنید
  • از الگوی «show more» به جای render کردن همه آیتم‌ها استفاده کنید
  • کامپوننت‌های مودال و dropdown رو فقط وقتی لازمن render کنید

CSS Containment

با خاصیت contain در CSS می‌تونید به مرورگر بگید تغییرات یه عنصر روی بقیه صفحه تأثیر نداره و لازم نیست کل صفحه رو دوباره محاسبه کنه:

/* Tell browser this element's layout is independent */
.product-card {
  contain: layout style paint;
}

/* For elements that are off-screen but in DOM */
.below-fold-section {
  content-visibility: auto;
  contain-intrinsic-size: 0 500px; /* estimated height */
}

/* Practical example: A list of cards */
.card-container {
  contain: content; /* shorthand for layout + style + paint */
}

.card-container .card {
  contain: layout style;
}

خاصیت content-visibility: auto واقعاً قدرتمنده و به نظرم کمتر از حقش بهش توجه میشه. مرورگر عناصری که خارج از viewport هستن رو اصلاً render نمی‌کنه تا وقتی که کاربر بهشون نزدیک بشه. این هم render اولیه صفحه رو سریع‌تر می‌کنه و هم INP رو بهبود میده چون DOM مؤثر کوچک‌تر میشه.

اندازه‌گیری INP: ابزارها و روش‌ها

یه قانون ساده: نمی‌تونید چیزی رو بهینه‌سازی کنید که اندازه‌گیری‌ش نکردید. خوشبختانه ابزارهای خوبی برای این کار وجود داره.

Chrome DevTools — پنل Performance

در Chrome DevTools، تب Performance رو باز کنید و یه recording بگیرید. حین recording با صفحه تعامل کنید — کلیک کنید، تایپ کنید، اسکرول کنید. بعد از توقف recording، می‌تونید هر تعامل رو ببینید و سه فاز INP (Input Delay، Processing Time، Presentation Delay) رو به صورت تفکیک‌شده مشاهده کنید.

همچنین بخش «Interactions» رو تو تب Performance فعال کنید. تعاملاتی که INP بالایی دارن با رنگ قرمز مشخص میشن و سریع جلب توجه می‌کنن.

کتابخانه web-vitals.js

این کتابخانه رسمی گوگل ساده‌ترین راه برای اندازه‌گیری Core Web Vitals در محیط production هست:

import { onINP } from 'web-vitals';

// Basic usage
onINP((metric) => {
  console.log('INP:', metric.value, 'ms');
  console.log('Rating:', metric.rating); // 'good', 'needs-improvement', or 'poor'

  // Send to your analytics
  sendToAnalytics({
    name: metric.name,
    value: metric.value,
    rating: metric.rating,
    id: metric.id,
    navigationType: metric.navigationType,
  });
});

// Advanced: Get attribution data to find the cause
import { onINP } from 'web-vitals/attribution';

onINP((metric) => {
  const attribution = metric.attribution;

  console.log('INP Breakdown:');
  console.log('  Input Delay:', attribution.inputDelay, 'ms');
  console.log('  Processing Time:', attribution.processingDuration, 'ms');
  console.log('  Presentation Delay:', attribution.presentationDelay, 'ms');
  console.log('  Target Element:', attribution.interactionTarget);
  console.log('  Interaction Type:', attribution.interactionType);

  // Now you know EXACTLY which element and which phase to optimize
  sendDetailedAnalytics({
    inp: metric.value,
    inputDelay: attribution.inputDelay,
    processingTime: attribution.processingDuration,
    presentationDelay: attribution.presentationDelay,
    element: attribution.interactionTarget,
    type: attribution.interactionType,
    page: window.location.pathname,
  });
});

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

PerformanceObserver API

اگه مانیتورینگ سفارشی‌تری می‌خواید، PerformanceObserver کنترل کامل رو بهتون میده:

// Monitor all interactions and their durations
const observer = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    // Only look at entries with an interaction ID
    if (entry.interactionId) {
      const duration = entry.duration;
      const startTime = entry.startTime;
      const target = entry.target;

      console.log(`Interaction detected:`, {
        type: entry.name,
        duration: `${duration}ms`,
        target: target?.tagName,
        inputDelay: entry.processingStart - entry.startTime,
        processingTime: entry.processingEnd - entry.processingStart,
        presentationDelay: entry.startTime + entry.duration - entry.processingEnd,
      });

      // Alert on slow interactions
      if (duration > 200) {
        console.warn(`Slow interaction: ${entry.name} took ${duration}ms`);
        reportSlowInteraction(entry);
      }
    }
  }
});

observer.observe({
  type: 'event',
  buffered: true,
  durationThreshold: 16 // Report events longer than 16ms (one frame)
});

// Helper to build a custom INP tracking system
class INPTracker {
  constructor() {
    this.interactions = new Map();
    this.longestInteraction = 0;
    this.init();
  }

  init() {
    const observer = new PerformanceObserver((list) => {
      for (const entry of list.getEntries()) {
        if (!entry.interactionId) continue;

        const existing = this.interactions.get(entry.interactionId);

        if (existing) {
          if (entry.duration > existing.duration) {
            this.interactions.set(entry.interactionId, entry);
          }
        } else {
          this.interactions.set(entry.interactionId, entry);
        }
      }

      this.calculateINP();
    });

    observer.observe({ type: 'event', buffered: true, durationThreshold: 16 });
  }

  calculateINP() {
    const entries = [...this.interactions.values()];
    entries.sort((a, b) => b.duration - a.duration);

    const index = Math.min(entries.length - 1, Math.floor(entries.length * 0.98));
    const inp = entries[index]?.duration || 0;

    if (inp !== this.longestInteraction) {
      this.longestInteraction = inp;
      console.log(`Current INP estimate: ${inp}ms`);
    }
  }
}

این کلاس INPTracker یه پیاده‌سازی ساده‌شده از نحوه محاسبه INP هست. تو production بهتره از web-vitals.js استفاده کنید، ولی فهمیدن این کد بهتون درک خیلی عمیق‌تری از مکانیزم INP میده.

نکته مهم درباره سافاری در ۲۰۲۶

همون‌طور که قبلاً اشاره کردم، تو سال ۲۰۲۶ سافاری هم قراره LCP و INP رو پشتیبانی کنه. این خبر خیلی بزرگیه. دیگه نمی‌تونید بگید «داده‌های ما فقط از کروم و فایرفاکسه و نماینده همه کاربران نیست». با اضافه شدن سافاری، تقریباً تمام مرورگرهای اصلی این معیارها رو گزارش می‌کنن و داده‌هاتون دقیق‌تر و جامع‌تر میشه.

پس الان بهترین زمانه که زیرساخت مانیتورینگ INP رو آماده کنید.

چک‌لیست عملی بهینه‌سازی INP

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

  1. اندازه‌گیری وضعیت فعلی: قبل از هر کاری، INP فعلی سایتتون رو با web-vitals.js اندازه بگیرید و attribution data رو جمع‌آوری کنید تا بدونید مشکل کجاست.
  2. شناسایی بدترین تعاملات: از داده‌های attribution، عنصرها و صفحاتی که بیشترین INP رو دارن شناسایی کنید. روی اونا تمرکز کنید — نه روی همه‌چیز.
  3. تحلیل سه فاز: مشخص کنید مشکل اصلی Input Delay هست یا Processing Time یا Presentation Delay. هر کدوم راه‌حل متفاوتی داره.
  4. Long Taskها رو بشکنید: از scheduler.yield() یا scheduler.postTask() استفاده کنید. برای مرورگرهای قدیمی‌تر، fallback به setTimeout بذارید.
  5. Event handlerها رو بهینه کنید: debounce و throttle. Layout thrashing رو حذف کنید. از requestAnimationFrame برای آپدیت‌های بصری استفاده کنید.
  6. محاسبات سنگین رو به Web Worker بفرستید: هر چیزی که بیشتر از ۵۰ میلی‌ثانیه طول می‌کشه و به DOM دسترسی نداره، کاندیدای خوبی برای Worker هست.
  7. از ابزارهای فریمورک استفاده کنید: در React از useTransition و useDeferredValue بهره ببرید. برای لیست‌های بزرگ حتماً virtual scrolling پیاده‌سازی کنید.
  8. DOM رو سبک کنید: عناصر غیرضروری رو حذف کنید. از content-visibility: auto برای محتوای خارج از viewport استفاده کنید. CSS containment رو اعمال کنید.
  9. اسکریپت‌های third-party رو مدیریت کنید: غیرضروری‌ها رو حذف کنید. بقیه رو با async یا defer بارگذاری کنید و بررسی کنید آیا main thread رو بلاک می‌کنن.
  10. روی موبایل تست کنید: یادتون باشه فقط ۶۴.۹٪ سایت‌های موبایل INP خوب دارن. حتماً با CPU throttling تست کنید و از دستگاه‌های واقعی میان‌رده استفاده کنید.
  11. مانیتورینگ مداوم: یه داشبورد بسازید که INP رو real-time مانیتور کنه. alert تنظیم کنید که اگه INP از ۲۰۰ms رد شد، خبردار بشید.

جمع‌بندی

INP تغییر بزرگی در نحوه ارزیابی عملکرد وبه. برخلاف FID که فقط اولین تعامل رو بررسی می‌کرد، INP کل تجربه تعاملی کاربر رو در نظر می‌گیره. و همون‌طور که کیس‌استادی‌های Trendyol، redBus و Hotstar نشون دادن، بهبود INP مستقیماً به افزایش نرخ کلیک، فروش و بازدید ترجمه میشه.

نکته کلیدی اینه: INP یه مشکل واحد نیست — ترکیبی از سه فاز هست و هر کدوم راه‌حل خودش رو داره. اول اندازه بگیرید، بعد تحلیل کنید، بعد هدفمند بهینه‌سازی کنید.

از ابزارهای مدرن مثل scheduler.yield()، Web Workers و hook‌های React مثل useTransition استفاده کنید. حجم DOM رو کنترل کنید و CSS containment رو فراموش نکنید.

با پشتیبانی سافاری از INP در ۲۰۲۶ و ویژگی‌های جدید React 19.2، الان واقعاً بهترین زمانه که INP رو جدی بگیرید. سایت‌هایی که امروز این کار رو انجام بدن، هم در رتبه‌بندی گوگل مزیت دارن و هم کاربران راضی‌تری خواهند داشت.

یادتون باشه: هر میلی‌ثانیه مهمه. وقتی کاربر کلیک می‌کنه و ظرف ۲۰۰ میلی‌ثانیه پاسخ می‌گیره، احساس می‌کنه سایت زنده‌ست و بهش گوش میده. ولی وقتی نیم ثانیه یا بیشتر طول بکشه؟ حس می‌کنه سایت خراب یا کنده. و این حس مستقیماً تو تصمیمش برای موندن یا رفتن تأثیر می‌ذاره.

موفق باشید و INP خوبی داشته باشید!

درباره نویسنده Editorial Team

Our team of expert writers and editors.