Gernar
Frontend DeveloperJavaScript, производительность

Интервью-вопрос

Что такое сложное вычисление

Сложное вычисление заметно занимает процессор и может блокировать главный поток браузера. На интервью важно показать не только определение, но и способы снизить риск для UI.

Добавлен
Редакция

Подготовьте короткий ответ и пару деталей на случай уточняющих вопросов.

🐰0
🥚0

Мини-квиз

Проверка перед разбором

Несколько быстрых вопросов перед разбором. Так проще поймать места, которые только кажутся понятными.

Вопрос 1 из 50 правильно

Что ответить, если Promise не спас тяжелый расчет?

Вы объясняете, почему UI все равно зависает после оборачивания цикла в async-функцию.

Варианты ответа

Разбор

Разобраться, а не зазубрить

Дальше разбираем суть, типичные уточнения и места, где легко сказать лишнее или перепутать термины.

Базовая идея

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

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

Хороший короткий ответ на интервью:

Сложное вычисление это CPU-задача, которая на реальном объеме данных занимает заметное время и может блокировать главный поток. Во frontend я смотрю на размер данных, частоту запуска и влияние на UI. Дальше выбираю решение: уменьшаю объем работы, улучшаю алгоритм, кеширую результат, откладываю или разбиваю расчет, либо выношу его в Web Worker.

Почему это влияет на интерфейс

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

Плохой пример:

// Плохо: Promise не переносит расчет в отдельный поток.
async function handleSearch(items, query) {
  await Promise.resolve();

  const result = items
    .filter((item) => expensiveMatch(item, query))
    .sort(expensiveCompare);

  setResult(result);
}

Здесь await может поменять момент запуска кода, но сама фильтрация и сортировка все равно выполняются в главном потоке. Если данных много, поле поиска может зависать после каждого ввода.

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

Как выбрать оптимизацию

Не стоит отвечать "я всегда использую Web Worker" или "я всегда использую useMemo". Сильнее звучит ответ с критерием выбора. Разные инструменты решают разные проблемы.

Как выбрать подход

1Расчет повторяется с теми же входными данными?
Сначала проверьте мемоизацию или кеш, но следите за актуальностью данных.
2Алгоритм плохо растет на больших данных?
Ищите более дешевый алгоритм или уменьшайте входной набор до расчета.
3Проблема возникает при вводе пользователя?
Добавьте debounce, throttle или переносите расчет после критичного обновления UI.
4Один расчет долго занимает главный поток?
Рассмотрите Web Worker или разбиение работы на маленькие чанки.

Если расчет долгий только потому, что он повторяется без изменения входных данных, помогает кеш или useMemo. Если один запуск занимает сотни миллисекунд и блокирует ввод, мемоизация не спасет этот запуск. Тогда нужны Worker, chunking или изменение алгоритма.

Практический пример в React

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

function Products({ products, filters }) {
  const visibleProducts = useMemo(() => {
    return filterAndSortProducts(products, filters);
  }, [products, filters]);

  return <ProductList items={visibleProducts} />;
}

Такой код может убрать лишние повторные расчеты. Но если filters создается заново на каждом рендере, зависимость будет новой и кеш не сработает. А если сам расчет занимает слишком много времени при первом запуске, он все равно пройдет в главном потоке.

Для долгой обработки данных можно использовать Worker. В React важно не забыть про жизненный цикл. Иначе Worker останется работать после размонтирования компонента, а старый результат может перезаписать новый UI.

useEffect(() => {
  let cancelled = false;
  const worker = new Worker(new URL("./search-worker.js", import.meta.url), {
    type: "module",
  });

  setStatus("loading");
  worker.postMessage({ items, query });

  worker.onmessage = (event) => {
    if (cancelled) return;
    setResult(event.data);
    setStatus("ready");
  };

  worker.onerror = () => {
    if (cancelled) return;
    setStatus("error");
  };

  return () => {
    cancelled = true;
    worker.terminate();
  };
}, [items, query]);

Это не делает алгоритм магически дешевым, но освобождает главный поток для UI. Минусы тоже есть: данные нужно передавать сообщениями, Worker не имеет прямого доступа к DOM, а для больших объектов сама передача может стоить заметного времени. Без статуса загрузки и ошибки пользователь может видеть старые данные и не понять, идет ли расчет.

Безопасный ход рассуждения

Опасный путь
  1. 1Запустить тяжелый цикл прямо в обработчике клика
  2. 2Обновить состояние только после завершения расчета
  3. 3Не дать пользователю отменить действие
  4. 4Получить подвисший ввод и пропущенные кадры
Безопаснее
  1. 1Измерить время расчета на реальном объеме данных
  2. 2Уменьшить работу или кешировать повторный результат
  3. 3Вынести долгий расчет в Worker или дробить его
  4. 4Показывать прогресс, отмену или состояние загрузки

На интервью удобно идти от симптома к решению. Сначала скажите, что именно может сломаться: input lag, пропущенные кадры, долгое открытие страницы, зависший обработчик клика. Потом объясните, как проверить гипотезу: профайлер, реальные данные, слабое устройство, частота запуска.

После этого выбирайте самое простое решение, которое убирает причину. Иногда лучший ответ не Worker, а нормальный алгоритм или уменьшение объема данных до того, как они попадут в компонент.

Что еще стоит уточнить

Не всякая долгая операция является сложным вычислением. Ожидание сети может занимать секунды, но CPU в это время не обязательно занят. Однако обработка большого JSON после ответа сервера уже может стать тяжелой CPU-задачей.

Еще важно различать редкий и частый запуск. Разовая сортировка при открытии экрана может быть приемлемой, если UI показывает состояние загрузки. Та же сортировка после каждого символа в поле поиска уже может стать заметной проблемой.

Если хотите усилить ответ, добавьте одну фразу про измерения: "Я бы подтвердил это в Performance panel или React Profiler, потому что без измерений легко оптимизировать не то место".

Частые ошибки

Где обычно ошибаются

Проверьте формулировки, которые звучат уверенно, но на интервью быстро выдают пробелы.

  1. 1

    Путать асинхронность и параллельность

    async/await не переносит CPU-работу в другой поток. Если внутри async-функции идет долгий цикл, главный поток все равно занят, а UI может зависнуть. На интервью лучше сказать, что для фонового расчета нужен Web Worker или разбиение работы на части.
  2. 2

    Мемоизировать все подряд

    useMemo полезен, когда расчет действительно дорогой и зависимости стабильны. Если зависимости каждый раз создаются заново, кеш почти не помогает и усложняет код. Сначала измерьте проблему, потом добавляйте мемоизацию.
  3. 3

    Не учитывать размер данных

    Код может быть быстрым на 20 элементах и ломать UX на 20 000. На интервью лучше связывать сложное вычисление с ростом входа, частотой запуска и устройствами пользователей. Это звучит практичнее, чем просто сказать "долго работает".
  4. 4

    Забывать про стоимость передачи в Worker

    Worker помогает не блокировать UI, но данные нужно отправить сообщением и получить обратно. Для больших объектов сериализация может быть заметной. Иногда выгоднее передавать компактный формат, использовать Transferable или оптимизировать алгоритм на главном потоке.
  5. 5

    Не защищать UI от устаревшего результата

    Если пользователь быстро меняет фильтр или запрос, старый расчет может завершиться позже нового и перезаписать состояние. Для долгих вычислений нужны отмена, проверка актуальности результата и понятные состояния loading и error.
  6. 6

    Оптимизировать без измерений

    Без профайлера легко улучшать не то место. В сильном ответе стоит упомянуть измерения: Performance panel, React Profiler, реальные данные и слабые устройства. Так вы показываете, что не добавляете сложность в код вслепую.

Follow-up

Что могут спросить дальше

Короткие ответы на вопросы, которыми проверяют понимание вычислений, главного потока и оптимизации UI.

Живые ответы

Видео с похожим вопросом

Если найдем публичные интервью с таким вопросом, добавим их сюда. Их удобно смотреть после теории, чтобы свериться с живыми ответами.

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

Содержание