Batch обновления

Пакетная (batch) обработка обновлений снижает количество операций с DOM и уменьшает количество reflow-циклов браузера.


Проблема индивидуальных обновлений

Если обновлять каждый элемент независимо:

elements.forEach(el => {
  el.textContent = format(el.getAttribute('datetime'), 'ru');
});

Это вызывает N последовательных операций записи в DOM. Браузер может выполнять Recalculate Style после каждой записи (если читается геометрия между записями).


requestAnimationFrame для пакетирования

import { format } from 'timeago.js';

function batchUpdate(elements, locale = 'ru') {
  requestAnimationFrame(() => {
    elements.forEach(el => {
      const date = el.getAttribute('datetime');
      if (!date) return;

      const newText = format(date, locale);
      if (el.textContent !== newText) {
        el.textContent = newText;
      }
    });
  });
}

requestAnimationFrame гарантирует выполнение обновлений перед следующим кадром браузера — в единой операции.


DocumentFragment при добавлении новых элементов

import { format } from 'timeago.js';

function addPosts(posts, container) {
  const fragment = document.createDocumentFragment();

  posts.forEach(post => {
    const li = document.createElement('li');
    li.innerHTML = `
      <h3>${post.title}</h3>
      <time datetime="${post.createdAt}">
        ${format(post.createdAt, 'ru')}
      </time>
    `;
    fragment.appendChild(li);
  });

  // Одна операция добавления в DOM вместо N
  container.appendChild(fragment);
}

Пакетная инициализация render

import { render } from 'timeago.js';

function initBatch(selector = '[datetime]', locale = 'ru', batchSize = 50) {
  const nodes = Array.from(document.querySelectorAll(selector));
  let index   = 0;

  function processBatch() {
    const batch = nodes.slice(index, index + batchSize);
    render(batch, locale);
    index += batchSize;

    if (index < nodes.length) {
      requestAnimationFrame(processBatch);
    }
  }

  requestAnimationFrame(processBatch);
}

Инициализация 50 элементов за кадр — UI не блокируется при большом количестве.


Единый таймер для всех элементов

Вместо отдельного таймера на каждый элемент — один общий:

import { format } from 'timeago.js';

function createBatchRenderer(locale = 'ru') {
  let elements = new Set();
  let timer;

  function getMinInterval() {
    let min = Infinity;
    elements.forEach(el => {
      const diff = Math.abs(Date.now() - new Date(el.getAttribute('datetime')).getTime());
      let interval;
      if (diff < 60000)    interval = 10000;
      else if (diff < 3600000)  interval = 60000;
      else if (diff < 86400000) interval = 1800000;
      else interval = 86400000;
      min = Math.min(min, interval);
    });
    return min === Infinity ? 60000 : min;
  }

  function tick() {
    requestAnimationFrame(() => {
      elements.forEach(el => {
        const date = el.getAttribute('datetime');
        if (!date) return;

        const next = format(date, locale);
        if (el.textContent !== next) el.textContent = next;
      });
    });

    timer = setTimeout(tick, getMinInterval());
  }

  return {
    add(el) {
      elements.add(el);
      if (elements.size === 1) tick();
    },
    remove(el) {
      elements.delete(el);
      if (elements.size === 0) clearTimeout(timer);
    },
    destroy() {
      clearTimeout(timer);
      elements.clear();
    }
  };
}

Пакетные обновления при получении данных

import { format } from 'timeago.js';

async function loadAndRenderPosts(page) {
  const posts = await fetch(`/api/posts?page=${page}`).then(r => r.json());

  const container = document.getElementById('feed');
  const fragment  = document.createDocumentFragment();

  // Сначала создать все элементы (без DOM)
  posts.forEach(post => {
    const li = document.createElement('li');
    li.innerHTML = `
      <h3>${post.title}</h3>
      <time datetime="${post.createdAt}">
        ${format(post.createdAt, 'ru')}
      </time>
    `;
    fragment.appendChild(li);
  });

  // Одна вставка в DOM
  container.appendChild(fragment);

  // Подключить автообновление для новых элементов
  const newTimes = container.querySelectorAll('li:last-child time, li:nth-last-child(-n+' + posts.length + ') time');
  // Более надёжный способ — через добавленный fragment
}

Пакетная отмена render

import { cancel } from 'timeago.js';

function cancelAll(selector = '[datetime]') {
  const nodes = document.querySelectorAll(selector);
  cancel(nodes); // одна операция отмены для всех
}

React: батчинг через flushSync

При добавлении множества элементов в React используется автоматический батчинг (React 18+):

import { startTransition } from 'react';

function addManyPosts(posts) {
  startTransition(() => {
    // React батчит все setState-вызовы внутри
    setPosts(prev => [...prev, ...posts]);
  });
}

Состояние обновится одним ре-рендером, что снижает количество перерисовок.