Как управлять DOM для загрузки и отображения картинки с обработкой ошибок и состояния загрузки
Что проверяют
Умеете ли вы работать с событиями загрузки ресурса напрямую, без фреймворка, и продумываете ли крайние случаи: гонки, кеш, повторные ошибки. Следом часто просят обернуть загрузку в Promise или показать, как код поведёт себя при быстрой смене картинки.
Как отвечать
Базовая схема на чистом DOM:
- состояние: контейнер с атрибутом
data-state(loading,loaded,error), стили и текст зависят от него. На время загрузки ставитсяaria-busy="true", у картинки обязательныйalt; - место под картинку: заранее задайте
widthиheightилиaspect-ratioу контейнера, иначе после загрузки вёрстка прыгнет и вырастет CLS; - загрузка:
const img = new Image(), подписка наloadиerror, затемimg.src = url. Удобно обернуть это в функцию, возвращающуюPromise, или вызватьawait img.decode(): он дожидается и загрузки, и декодирования, поэтому вставка в DOM обходится без подёргивания; - ошибка: заглушка и кнопка «Повторить». Код ответа сервера событие
errorне сообщает. Если он нужен, картинку грузят черезfetch, проверяютresponse.ok, делаютURL.createObjectURL(blob)и потом не забываютURL.revokeObjectURL. Для чужого домена это потребует CORS; - гонки: если
srcменяется быстро, старая картинка может догрузиться позже новой. Храните счётчик запросов или текущий url и игнорируйте неактуальный результат.AbortControllerработает сfetch, а загрузку черезimg.srcтак не отменить; - уже существующий
<img>: если картинка из кеша загрузилась до подписки, событие потеряно. Проверяйтеimg.complete, а битую картинку выдаётnaturalWidth === 0.
Для картинок ниже первого экрана добавьте loading="lazy".
Где спотыкаются
Ставят в onerror запасной src, который тоже не грузится. Получается бесконечный цикл запросов, поэтому обработчик снимают перед подменой.
Не защищаются от гонок: пользователь быстро листает галерею, а на экране оказывается не та картинка, что выбрана.
Показывают индикатор и картинку без зарезервированного места. Спиннер высотой 20 пикселей сменяется фото на 600, и контент под ним уезжает вниз.