Перейти к содержанию

Стоимость JavaScript и порционная обработка

В предыдущем уроке мы увидели, почему время обработчика не описывает весь отклик. Теперь изменим необходимую вычислительную работу так, чтобы браузер получал возможности обработать другие задачи. Одновременно сохраним правильность: старый запрос не должен перезаписывать карточки, соответствующие новому значению поля.

Пример использует тот же speed-lab и искусственную сумму по 2500 записям. Это демонстрационная нагрузка, не поиск ProfessorWeb. Автор не запускал код и не измерял скорость. Размер порции 50 и число проходов checksum — учебные параметры; их эффект читатель проверяет вручную, а для производственной функции сначала обосновывает необходимость самой работы.

Размер файла и стоимость исполнения

Передача JavaScript — только одна часть цены. После загрузки браузер обрабатывает и выполняет программу, а вызванные операции могут изменять документ. Небольшой файл способен начать длительный цикл; большой модуль может частично оставаться неиспользованным в выбранном сценарии. Поэтому размер доставки и работа основного потока должны быть отдельными наблюдениями.

У speed-lab модуль создаёт синтетический набор и фильтрует его. Проверяйте не только загрузочный водопад, но и ввод после завершения загрузки. Уменьшение байтов файла никак не отменяет вычисления внутри обработчика. Сжатие полезно для своей фазы, а устройство алгоритма — для своей. Смешивание этих действий затрудняет причинный разбор.

Долгая задача не даёт другим операциям основного потока свободно вклиниться внутрь её синхронного участка. В инструментах можно исследовать такие интервалы и связанные функции. Порог Long Tasks API связан с задачами дольше 50 миллисекунд, но наличие этого сигнала не означает автоматическое нарушение INP: нужны сценарий и связь с взаимодействием. Long Tasks API.

Передача управления

Добавление await не всегда создаёт нужную паузу. Немедленно разрешённый Promise продолжает через микрозадачи, и длительная цепочка таких продолжений способна оставить браузер без ожидаемой возможности показать кадр. Для разделения работы нужен механизм планирования, подходящий этой цели. scheduler.yield() предназначен для уступки управления с дальнейшим продолжением, но его поддержка ограничена. Scheduler.yield.

В candidate есть функция с проверкой наличия API и простым запасным вариантом:

function yieldToBrowser() {
  if (globalThis.scheduler?.yield) return globalThis.scheduler.yield();
  return new Promise((resolve) => setTimeout(resolve, 0));
}

Запасной таймер не обещает немедленного исполнения и не повторяет все свойства планировщика. Он позволяет закончить текущий синхронный участок и планирует дальнейшую работу. Браузер самостоятельно выбирает возможности обработки задач и показа; это не контракт одного гарантированного кадра после каждого вызова. Ограничения таймеров также зависят от состояния документа. Поведение setTimeout.

Не помещайте весь тяжёлый расчёт внутрь requestAnimationFrame в надежде сделать его фоновым. Эта функция связана с подготовкой кадра, и большая синхронная работа в её обработчике может задержать отображение. Подходящая граница нужна для конкретной операции; название API не отменяет её стоимость. Для анимации и для обработки большого набора выбор может различаться.

Порции и ревизия запроса

Ниже полный обработчик candidate и его вспомогательные определения. Функции records, checksum и render остаются импортированными из общего модуля; в HTML уже есть поле и статус. Код заменяет синхронный фильтр предыдущего урока:

const input = document.querySelector("#filter");
const status = document.querySelector("#status");
let revision = 0;

async function filter() {
  const ownRevision = ++revision;
  const query = input.value.trim().toLocaleLowerCase("ru");
  status.textContent = "Обновляем карточки…";
  await yieldToBrowser();
  let value = 0;
  const matches = [];
  for (let start = 0; start < records.length; start += 50) {
    if (ownRevision !== revision) return;
    for (const record of records.slice(start, start + 50)) {
      value = (value + checksum(record.title)) >>> 0;
      if (record.title.toLocaleLowerCase("ru").includes(query)) matches.push(record);
    }
    await yieldToBrowser();
  }
  if (ownRevision === revision) render(matches, value);
}
input.addEventListener("input", filter);
filter();

Каждое новое событие увеличивает общую ревизию. Текущий обработчик сохраняет свою версию и запрос в локальных переменных. Перед очередной порцией он сравнивает версии; если поле уже породило новый расчёт, старая обработка прекращается. Перед итоговым показом выполняется ещё одна проверка. Так запоздалый ответ не заменяет актуальные карточки.

Проверка выполняется между порциями, а не внутри каждой операции checksum. Если одна запись сама занимает много времени, деление только по числу записей может оказаться недостаточным. Тогда требуется изменить внутреннюю работу либо выбрать другую границу. Размер 50 взят для учебного объяснения; измерение на целевых устройствах определяет подходящее решение.

Порционная обработка не обязана уменьшать общее время до завершения всех вычислений. Появляются дополнительные границы и расходы планирования. Цель изменения — дать интерфейсу возможности реагировать, сохранив результат. Поэтому одновременно сравнивайте отзывчивость и момент готовности актуального набора, вместо того чтобы трактовать любое увеличение общего времени как провал.

Проверка правильности

Введите Markdown, затем быстро CSS, затем очистите поле. Ожидается соответствие карточек последнему запросу, а не последнему завершившемуся старому обработчику. Для обеих тем подготовленный набор содержит по 500 совпадений, для пустого запроса — 2500. Первые 30 остаются в том же порядке, а учебная сумма одинакова при полном завершении.

Эти значения основаны на структуре набора; автор не проверял их исполнением. Читатель должен сопоставить видимый результат и сообщения статуса. Если новый запрос появляется после окончания старого, он всё равно запускает следующую обработку. Если старая версия остановилась, статус новой остаётся владельцем состояния ожидания, и старый обработчик его не очищает.

Не показывайте частичный набор как окончательный без явного договора интерфейса. Здесь мы накапливаем совпадения локально и заменяем результат после завершения. Можно проектировать постепенную выдачу, но тогда требуются правила порядка, состояния готовности и отмены. Это отдельное изменение поведения, которое не нужно скрывать внутри технического деления цикла.

Для настоящего тяжёлого вычисления рассмотрите, можно ли уменьшить работу, заранее подготовить данные или перенести подходящую часть в worker. Worker не имеет того же прямого доступа к DOM, а обмен данными тоже имеет стоимость. Поисковая серия уже раскрывает более сложное устройство поиска по сайту; здесь наша задача ограничена уступкой управления и актуальностью результата.

Цена дополнительной асинхронности

Каждая порция создаёт промежуточное состояние: вычисление ещё не завершено, поле уже могло измениться, а новый обработчик должен владеть сообщением. Поэтому добавление уступки управления сопровождается договором состояния. Если программа имеет другие ошибки, их нельзя скрывать вечным сообщением ожидания. В более сложном приложении предусмотрите завершение с понятным результатом и отдельную обработку неудачной операции.

Наш фильтр не делает сетевых запросов и использует неизменный подготовленный набор. Поэтому номер ревизии достаточно просто сравнивать с последним вводом. Если данные обновляются параллельно, понадобится также согласовать версию набора. Один счётчик запроса не доказывает свежесть всей информации. Такое расширение следует вводить отдельным шагом после освоения базового примера, чтобы причины отмены оставались понятными.

Порции также требуют наблюдения за памятью и числом создаваемых объектов. Здесь slice даёт небольшой временный массив, а совпадения хранятся до окончания обработки. Для более крупного набора можно выбрать другой обход, но сначала оцените, является ли это действительным ограничением. Переписывание очевидного алгоритма на менее читаемый без оснований усложнит учебный материал и не гарантирует полезного эффекта.

Сохраните запись с длительностью порций и правильным последним запросом. Если взаимодействие стало отзывчивее, но результат готовится дольше, опишите этот обмен явно. Если изменений не видно, проверьте фактическую нагрузку и ограничение после вычислений. Такая оценка завершает работу с основным потоком и позволяет исследовать другую независимую переменную — повторную доставку ресурсов через HTTP-кеш.

Оглавление курса