Случалось ли вам когда-нибудь тонуть в море неупорядоченных фотографий? Лично я не раз с этим сталкивалась!
Исследуя возможности генеративного искусственного интеллекта, я экспериментировала с приложением для поиска фотографий и успешно нашла необходимые изображения. И вдруг меня осенило: что, если в папке окажутся тысячи фотографий?
Вопрос далеко не тривиальный — представьте, сколько фото своих пушистых питомцев ежедневно делают ваши друзья. Это открытие подтолкнуло меня изучить, можно ли работать с большими коллекциями изображений без перегрузки системных ресурсов.
Функции оригинального приложения: незамысловатое начало
Приложение начинало работу с двух ключевых функций:
- Загрузка всех изображений из выбранной папки.
- Поиск фотографий по контенту. Отправка изображений на бэкенд для фильтрации и возврата наиболее точных совпадений.
Вот сокращенный код, на котором все это работает:
// Загрузка всех изображений из выбранной папки
async handleFolderSelect() {
const dirHandle = await window.showDirectoryPicker();
const images = [];
// Загрузка всех изображений сразу
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && entry.name.match(/\.(jpg|jpeg|png|gif)$/i)) {
const file = await entry.getFile();
const blobUrl = URL.createObjectURL(file);
images.push({
path: blobUrl,
name: entry.name,
handle: entry
});
}
}
this.images = images;
this.allImages = images;
}
// Поиск изображений по содержанию
async searchImages() {
// Создание объекта FormData для отправки всех изображений
const formData = new FormData();
formData.append('query', this.searchQuery);
// Добавление всех текущих изображений в запрос
for (const image of this.images) {
if (image.handle) {
// Получение фактического файла из handle
const file = await image.handle.getFile();
formData.append('images[]', file, image.name);
}
}
const response = await fetch('/search', {
method: 'POST',
body: formData
});
// Обновление изображений результатами поиска
this.images = ...
}
Это приложение прекрасно функционирует с небольшими папками, однако при работе с тысячами фотографий возникают сбои.
Проблемы с большими папками
Работа с папкой, содержащей более 1000 изображений, — непростая задача.
Вот основные проблемы:
- Снижение производительности. Загрузка всех изображений одновременно может замедлить работу всех систем, от браузера до сервера.
- Перегрузка памяти. Создание URL-адресов blob-объектов для 1000 изображений и отправка их всех на сервер одним запросом может привести к перегрузке памяти системы.
- Ухудшение пользовательского опыта. Длительное время ожидания и неотзывчивое приложение вызывают разочарование у пользователей, которым нужна обратная связь и интерактивность даже при работе с большими массивами данных.
Очевидно, что требовался оптимизированный подход.
Пакетная обработка: более интеллектуальная стратегия
Первый шаг к масштабируемости — пакетная обработка.
Преимущества пакетной обработки:
- Повышение уровня управления памятью: меньшие партии означают меньшее количество ресурсов, потребляемых одновременно.
- Эффективность работы сети: небольшие HTTP-запросы реже прерываются по времени, и их легче повторить в случае неудачи.
- Отзывчивый сервер: обработка небольших пакетов снижает риск возникновения ошибок, связанных с нехваткой памяти (OOM).
Обработка изображений небольшими партиями — скажем, от 50 до 100 за раз, — позволяет более эффективно управлять ресурсами.
Вот как это было реализовано:
async searchImages() {
const BATCH_SIZE = 50;
let allResults = [];
for (let i = 0; i < this.allImages.length; i += BATCH_SIZE) {
const batch = this.allImages.slice(i, i + BATCH_SIZE);
const formData = new FormData();
formData.append('query', this.searchQuery);
for (const image of batch) {
const file = await image.handle.getFile();
formData.append('images[]', file, image.name);
}
const response = await fetch('/search', { method: 'POST', body: formData });
const data = await response.json();
allResults = allResults.concat(data.results);
}
this.images = allResults.sort((a, b) => b.score - a.score);
}
Пакетная обработка обеспечивает столь необходимый контроль над использованием памяти и нагрузкой на сервер, но, похоже, не решает некоторые проблемы пользовательского интерфейса:
Улучшается ли пользовательский опыт? Обеспечивается ли немедленная обратная связь?

Прогрессивная загрузка: мгновенная обратная связь
Чтобы добиться большей интерактивности, я обратилась к прогрессивной загрузке. При таком подходе изображения обрабатываются и отображаются небольшими фрагментами (например, по 10 за раз), обеспечивая мгновенную обратную связь с пользователем.
Преимущества прогрессивной загрузки
- Улучшение пользовательского опыта: результаты появляются постепенно, что позволяет пользователям оставаться вовлеченными.
- Низкое потребление памяти: в каждый момент времени загружается небольшое количество изображений.
- Отзывчивый интерфейс: обновления происходят динамически, что позволяет избежать длительных задержек.
Вот как это работает:
async searchImages() {
const CHUNK_SIZE = 10;
this.isLoading = true;
this.images = [];
try {
for (let i = 0; i < this.allImages.length; i += CHUNK_SIZE) {
const chunk = this.allImages.slice(i, i + CHUNK_SIZE);
const results = await this.processImageChunk(chunk);
this.images = [...this.images, ...results].sort((a, b) => b.score - a.score);
await new Promise(resolve => setTimeout(resolve, 0)); // Let UI update
}
} finally {
this.isLoading = false;
}
}
Этот метод позволяет найти оптимальный баланс между производительностью и удовлетворенностью пользователей, особенно при работе с большими коллекциями.

Прогрессивная загрузка стала значительной оптимизацией, но выявила новые проблемы. Несмотря на успешную постепенную доставку результатов поиска, возникло несколько важных вопросов:
Как будет работать приложение, когда количество результатов поиска будет исчисляться тысячами?
Можно ли обеспечить плавное взаимодействие пользовательского интерфейса с таким большим набором данных?
Как наиболее эффективно отобразить тысячи изображений, не перегружая браузер?
Отображение на фронтенде: бесконечная прокрутка
Когда дело доходит до отображения больших коллекций изображений в веб-интерфейсе, ленивая загрузка в сочетании с бесконечной прокруткой предлагает элегантное решение. Загружая изображения только тогда, когда они вот-вот появятся в области просмотра, можно сэкономить память и повысить производительность.
Вот как я использовала Intersection Observer для ленивой загрузки:
mounted() {
this.observer = new IntersectionObserver(
(entries) => {
entries.forEach(async entry => {
if (entry.isIntersecting) {
const image = entry.target.__vue_data__;
if (image && !image.path) {
const file = await image.handle.getFile();
image.path = URL.createObjectURL(file);
}
}
});
},
{ root: null, rootMargin: '50px', threshold: 0.1 }
);
}
beforeUnmount() {
// Удаление всех наблюдателей
this.unobserveImages();
// Очистка blob-URL
this.clearBlobUrls();
// Отключение наблюдателя
this.observer.disconnect();
}
Эта реализация, основанная на сочетании методов HTML и JavaScript, позволяет:
- загружать изображения только тогда, когда они становятся видимыми в области просмотра;
- использовать заполнители для незагруженных изображений;
- предварительно загружать изображения немного раньше, чем они становятся видимыми;
- правильно обработать очистку компонентов.
При такой настройке изображения загружаются вовремя, пока пользователь прокручивает галерею, обеспечивая плавный просмотр.
Выбор подхода
Вот краткое описание наиболее подходящих вариантов использования:
Пакетная обработка идеальна, когда:
- точность поиска является приоритетом;
- работа ведется с небольшими коллекциями (<500 изображений);
- необходима точная сортировка результатов;
- обработка на стороне сервера имеет большее значение.
Прогрессивная загрузка подходит для:
- работы с очень большими коллекциями (1000+ изображений);
- обеспечения немедленной обратной связи;
- создания интерфейса, ориентированного на просмотр.
Бесконечная прокрутка отлично справится с такими задачами, как:
- автоматическая обработка ленивой загрузки по мере прокрутки;
- обеспечение бесперебойной работы.
Отметим, что в выборе стратегий работы с большими коллекциями файлов нет ничего правильного или неправильного. Эти подходы не являются взаимоисключающими — их можно комбинировать, чтобы использовать сильные стороны каждой стратегии.
Преимущества оптимизации фронтенда
Стратегический подход, ориентированный на фронтенд, дает значительные преимущества по сравнению с массовой загрузкой всего на бэкенд. Ограничение объема данных, отправляемых на сервер, позволяет:
- сократить сетевой трафик;
- снизить нагрузку на сервер;
- повысить контроль над использованием памяти на обеих сторонах;
- ускорить первоначальную загрузку;
- расширить возможности масштабируемости.
Эта оптимизация особенно важна для приложений с большим количеством изображений, где размер файлов может значительно повлиять на производительность.
Окончательный результат
Финальная оптимизированная версия выглядит следующим образом:
async handleFolderSelect() {
try {
const dirHandle = await window.showDirectoryPicker();
this.isLoading = true;
this.currentFolder = dirHandle.name;
this.allImages = [];
this.images = [];
// Очистка существующих URL blob'ов и удаление кэша изображений
this.clearBlobUrls();
this.unobserveImages();
// Постепенная обработка файлов
for await (const entry of dirHandle.values()) {
if (entry.kind === 'file' && entry.name.match(/\.(jpg|jpeg|png|gif)$/i)) {
// Первоначальное сохранение handle без создания URL blob'а
const imageData = {
name: entry.name,
handle: entry,
path: null // Будет создано по запросу
};
this.allImages.push(imageData);
}
}
// Загрузка первой партии изображений
const initialBatch = this.allImages.slice(0, 50);
for (const imageData of initialBatch) {
const file = await imageData.handle.getFile();
imageData.path = URL.createObjectURL(file);
}
// Обновление дисплея
this.images = this.allImages;
// Ожидание обновления DOM
await this.$nextTick();
// Начинаем наблюдать за изображениями
this.observeImages();
// Сохранение handle каталога для последующего доступа
this.$root.dirHandle = dirHandle;
} catch (error) {
console.error('Error:', error);
alert('Error accessing folder:' + error.message);
} finally {
this.isLoading = false;
}
},
async searchImages() {
const BATCH_SIZE = 50;
this.isLoading = true;
try {
let allResults = [];
const totalBatches = Math.ceil(this.allImages.length / BATCH_SIZE);
// Обработка изображений партиями
for (let i = 0; i < this.allImages.length; i += BATCH_SIZE) {
const currentBatch = Math.floor(i / BATCH_SIZE) + 1;
this.progress = `Processing batch ${currentBatch}/${totalBatches}`;
console.log('*** progress = ', this.progress);
const batch = this.allImages.slice(i, i + BATCH_SIZE);
const formData = new FormData();
formData.append('query', this.searchQuery);
for (const image of batch) {
if (image.handle) {
const file = await image.handle.getFile();
formData.append('images[]', file, image.name);
}
}
const batchResults = await fetch('/search', {
method: 'POST',
body: formData
}).then(r => r.json());
const curResults = batchResults.results.map(result => {
const existingImage = this.allImages.find(img => img.name === result.name);
const path = existingImage ? existingImage.path : URL.createObjectURL(new Blob([result.image_data], { type: 'image/jpeg' }));
return {
path: path,
name: result.name,
score: result.score,
handle: existingImage ? existingImage.handle : null
};
});
// Показ промежуточных результатов во время продолжения обработки
allResults = [...allResults, ...curResults];
this.images = allResults.sort((a, b) => b.score - a.score);
}
} catch (error) {
console.error('Search error:', error);
alert(error.message);
} finally {
this.isLoading = false;
}
},
Заключительные мысли
Это исследование при создании приложения для поиска изображений научило меня многому!
Главное, что я вынесла для себя, заключается в поиске оптимального соотношения между высокой производительностью, рациональным использованием памяти и удобством для пользователей. Независимо от того, используете ли вы пакетную обработку или прогрессивную загрузку, всегда есть возможность что-то улучшить.
Читайте также:
- JavaScript async/await: что хорошего, в чём опасность и как применять?
- 10 полезных библиотек для фронтенд-разработки
- Как Signal управляет состоянием приложения
Читайте нас в Telegram, VK и Дзен
Перевод статьи Jenny Ouyang: Quick Search of Hidden Photos With Optimization Approaches





