Современные фронтенд-приложения постоянно взаимодействуют с файлами.
Пользователи загружают аватары, перетаскивают видео в дашборды, экспортируют CSV-отчеты, просматривают PDF-файлы, скачивают сгенерированные конфигурационные файлы и работают с медиаконтентом прямо в браузере. На первый взгляд все это выглядит довольно просто: поле загрузки файла, элемент предпросмотра, возможно, кнопка скачивания — и задача решена.
Но именно здесь начинаются настоящие проблемы.
Большие файлы “замораживают” вкладку браузера. Изображения загружаются очень медленно. URL данных разрастаются до огромных размеров, а потребление памяти браузером постоянно растет. Предпросмотр может корректно работать с одним типом файлов, но ломаться с другим. В результате дашборд, который отлично показывал себя во время тестирования, теряет стабильность после обработки десятков изображений за одну сессию.
Именно в таких ситуациях Blob API становится не просто полезным, а необходимым инструментом.
Blob — это не просто небольшая браузерная функция для скачивания файлов. Это один из ключевых строительных блоков, лежащих в основе работы с файлами в современном JS. Понимание принципов работы Blob-объектов поможет создавать более чистые процессы загрузки файлов, безопасные механизмы предпросмотра, удобные инструменты экспорта файлов и интерфейсы с более эффективным управлением памятью.
В этой статье мы разберем практическое применение Blob в реальной фронтенд-разработке: создание Blob-объектов, обработку больших файлов по частям, сжатие изображений, создание предпросмотра, генерацию файлов для скачивания и предотвращение утечек памяти из-за забытых объектных URL.
❯ Что такое Blob?
Blob — это неизменяемый объект, представляющий сырые (raw) данные.
Эти данные могут быть текстом, JSON, CSV, изображением, видео, PDF или любым другим бинарным контентом, который способен хранить браузер. Слово Blob расшифровывается как Binary Large Object (“большой бинарный объект”), но во фронтенд-разработке его проще воспринимать как файловый контейнер, созданный или обрабатываемый внутри браузера.
Минимальный Blob выглядит так:
const textBlob = new Blob(['Hello from the Blob API'], { type: 'text/plain', });
Первый аргумент — это массив частей (chunks) данных. Второй аргумент описывает Blob-объект. В большинстве случаев самый важный параметр — type, который хранит MIME-тип содержимого.
Примеры распространенных MIME-типов:
text/plain application/json text/csv image/png image/jpeg application/pdf
MIME-тип помогает браузеру понять, как именно обрабатывать данные. JSON Blob, Blob с изображением и Blob с PDF-файлом могут быть обычными Blob-объектами, но браузер будет по-разному отображать, скачивать или отправлять их в зависимости от указанного типа.
❯ Почему Blob лучше больших URL данных
Распространенная ошибка новичков при генерации файлов — вручную создавать data: URL:
const hugeText = 'Some large content...'.repeat(100_000); const url = 'data:text/plain;charset=utf-8,' + encodeURIComponent(hugeText);
Это подходит для небольших примеров, но плохо масштабируется. Крупные URL данных могут потреблять гораздо больше памяти, чем кажется, создавать дополнительную нагрузку на браузер и в некоторых случаях ломаться из-за слишком большой длины URL.
В большинстве случаев Blob — гораздо более удачное решение:
const hugeText = 'Some large content...'.repeat(100_000); const blob = new Blob([hugeText], { type: 'text/plain', }); const url = URL.createObjectURL(blob);
Вместо кодирования всех данных в огромную строку URL браузер создает объектный URL, который внутри ссылается на данные Blob-объекта. Такой подход чище, эффективнее с точки зрения памяти и надежнее при работе с большим объемом данных.
❯ Как правильно создавать Blob-объекты
Создать Blob довольно просто, но в реальных проектах такую логику обычно выносят в небольшие утилиты, чтобы код оставался единообразным и удобным в поддержке.
type BlobOptionsInput = { mimeType?: string; }; function createFileBlob( parts: BlobPart[], options: BlobOptionsInput = {} ) { return new Blob(parts, { type: options.mimeType ?? 'text/plain', }); }
Теперь можно создавать разные файловые объекты с явно указанными MIME-типами.
Текстовый Blob
const readmeBlob = createFileBlob( ['This file was generated in the browser.'], { mimeType: 'text/plain' } );
JSON Blob
const userSettings = { theme: 'dark', compactMode: true, language: 'en', }; const settingsBlob = createFileBlob( [JSON.stringify(userSettings, null, 2)], { mimeType: 'application/json' } );
HTML Blob
const htmlDocument = ` <!doctype html> <html> <body> <h1>Generated HTML</h1> </body> </html> `; const htmlBlob = createFileBlob( [htmlDocument], { mimeType: 'text/html' } );
Главное правило здесь простое: всегда указываем корректный MIME-тип, если знаем, какой именно тип данных создаем.
❯ Скачивание сгенерированных файлов в браузере
Один из самых распространенных сценариев использования Blob — генерация файлов для скачивания без необходимости их создания на сервере.
Например, на странице настроек можно реализовать экспорт пользовательской конфигурации в формате JSON:
function downloadJsonFile( data: unknown, filename = 'settings.json' ) { const json = JSON.stringify(data, null, 2); const blob = new Blob([json], { type: 'application/json', }); const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); // Прим. пер.: эта часть кода отличается от оригинального. // Потенциально ссылка может быть отозвана до скачивания файла, // поэтому отзыв ссылки лучше отложить. // Ниже в оригинальном коде используется такой вариант setTimeout(() => { URL.revokeObjectURL(url); }, 0); }
Использование:
downloadJsonFile( { editor: 'VS Code', theme: 'dark', autosave: true, }, 'developer-settings.json' );
Этот подход особенно полезен для:
экспорта пользовательских настроек;
скачивания сгенерированных отчетов;
создания локальных резервных копий;
экспорта аналитических данных;
генерации JSON, CSV и текстовых файлов на стороне клиента.
Но здесь особенно важен один момент — очистка ресурсов:
URL.revokeObjectURL(url);
Каждый раз при создании объектного URL важно понимать, когда его нужно освободить (revoke), и не забывать это делать.
❯ Обработка больших файлов по частям
Чтение большого файла целиком может серьезно ухудшить производительность.
На первый взгляд этот код выглядит вполне безобидно:
const reader = new FileReader(); reader.onload = () => { const content = reader.result; console.log(content); }; reader.readAsText(file);
Для небольших файлов - да. Но при работе с большими файлами такой подход может привести к зависанию страницы или чрезмерному потреблению памяти.
Более безопасное решение — обработка файла по частям.
Объект File является разновидностью Blob, поэтому поддерживает метод slice(). Благодаря этому файл можно читать постепенно, по кускам.
async function processFileInChunks( file: File, chunkSize = 1024 * 1024 ) { const totalChunks = Math.ceil(file.size / chunkSize); for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) { const start = chunkIndex * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); await processChunk(chunk, chunkIndex, totalChunks); } }
Простой пример чтения файла по частям может выглядеть так:
function readBlobAsText(blob: Blob): Promise<string> { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => { resolve(String(reader.result ?? '')); }; reader.onerror = () => { reject(reader.error); }; reader.readAsText(blob); }); } async function processChunk( chunk: Blob, index: number, total: number ) { const text = await readBlobAsText(chunk); console.log({ index, total, size: chunk.size, preview: text.slice(0, 80), }); }
Такой подход позволяет лучше контролировать использование памяти и упрощает отображение прогресса обработки файла:
async function processFileWithProgress( file: File, onProgress: (percentage: number) => void ) { const chunkSize = 1024 * 1024; const totalChunks = Math.ceil(file.size / chunkSize); for (let index = 0; index < totalChunks; index++) { const start = index * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); await processChunk(chunk, index, totalChunks); const percentage = Math.round(((index + 1) / totalChunks) * 100); onProgress(percentage); } }
Обработка по частям особенно полезна для:
больших текстовых файлов;
логов;
импорта CSV;
загрузки видео;
браузерных инструментов для анализа файлов.
❯ Реализация загрузки файлов по частям
Загрузка файлов по частям — естественное продолжение работы с разбиением Blob на части.
Вместо отправки всего файла одним запросом, мы загружаем его по частям:
type ChunkUploadOptions = { endpoint: string; chunkSize?: number; onProgress?: (progress: { uploadedChunks: number; totalChunks: number; percentage: number; }) => void; }; class ChunkedFileUploader { private file: File; private endpoint: string; private chunkSize: number; private onProgress?: ChunkUploadOptions['onProgress']; constructor(file: File, options: ChunkUploadOptions) { this.file = file; this.endpoint = options.endpoint; this.chunkSize = options.chunkSize ?? 2 * 1024 * 1024; this.onProgress = options.onProgress; } async upload() { const uploadId = this.createUploadId(); const totalChunks = Math.ceil(this.file.size / this.chunkSize); for (let index = 0; index < totalChunks; index++) { const start = index * this.chunkSize; const end = Math.min(start + this.chunkSize, this.file.size); const chunk = this.file.slice(start, end); await this.uploadChunk(chunk, index, uploadId); this.onProgress?.({ uploadedChunks: index + 1, totalChunks, percentage: Math.round(((index + 1) / totalChunks) * 100), }); } return this.completeUpload(uploadId, totalChunks); } private async uploadChunk( chunk: Blob, index: number, uploadId: string ) { const formData = new FormData(); formData.append('chunk', chunk); formData.append('index', String(index)); formData.append('uploadId', uploadId); formData.append('filename', this.file.name); const response = await fetch(this.endpoint, { method: 'POST', body: formData, }); if (!response.ok) { throw new Error(`Failed to upload chunk ${index}`); } return response.json(); } private async completeUpload(uploadId: string, totalChunks: number) { const response = await fetch(`${this.endpoint}/complete`, { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ uploadId, totalChunks, filename: this.file.name, }), }); if (!response.ok) { throw new Error('Failed to complete upload'); } return response.json(); } private createUploadId() { return `${Date.now()}-${crypto.randomUUID()}`; } }
Использование:
const uploader = new ChunkedFileUploader(file, { endpoint: '/api/uploads', onProgress(progress) { console.log(`Uploaded ${progress.percentage}%`); }, }); await uploader.upload();
Разумеется, для запуска это примера нужен сервер. Сервер должен принимать части файла, временно их хранить и объединять после получения последнего куска. Но со стороны фронтенда именно разделение Blob на части лежит в основе такого подхода.
❯ Сжатие изображений с помощью Canvas и Blob
Еще одна область, где Blob особенно полезен, — загрузка изображений.
Прямая загрузка исходных изображений часто оказывается неэффективной. Фото с телефона может весить несколько мегабайт, а интерфейсу порой нужен только небольшой аватар или превью.
Распространенный подход во фронтенд-разработке выглядит так:
Загрузить изображение.
Отрисовать его на хосте (HTML-элемент
canvas).Изменить его размер.
Экспортировать данные холста как Blob.
Загрузить сжатый Blob.
Сначала создадим вспомогательную функцию для загрузки изображений:
function loadImageFromFile(file: File): Promise<HTMLImageElement> { return new Promise((resolve, reject) => { const url = URL.createObjectURL(file); const image = new Image(); image.onload = () => { URL.revokeObjectURL(url); resolve(image); }; image.onerror = () => { URL.revokeObjectURL(url); reject(new Error('Failed to load image')); }; image.src = url; }); }
Затем вычислим размеры с сохранением пропорций изображения:
function fitInsideBox( originalWidth: number, originalHeight: number, maxWidth: number, maxHeight: number ) { let width = originalWidth; let height = originalHeight; if (width > maxWidth) { height = Math.round((height * maxWidth) / width); width = maxWidth; } if (height > maxHeight) { width = Math.round((width * maxHeight) / height); height = maxHeight; } return { width, height }; }
Теперь сожмем изображение:
type ImageCompressionOptions = { maxWidth?: number; maxHeight?: number; quality?: number; outputType?: string; }; async function compressImageFile( file: File, options: ImageCompressionOptions = {} ): Promise<Blob> { const { maxWidth = 1600, maxHeight = 1000, quality = 0.82, outputType = 'image/jpeg', } = options; const image = await loadImageFromFile(file); const size = fitInsideBox( image.width, image.height, maxWidth, maxHeight ); const canvas = document.createElement('canvas'); canvas.width = size.width; canvas.height = size.height; const context = canvas.getContext('2d'); if (!context) { throw new Error('Canvas 2D context is not available'); } context.drawImage(image, 0, 0, size.width, size.height); return new Promise((resolve, reject) => { canvas.toBlob( blob => { if (!blob) { reject(new Error('Image compression failed')); return; } resolve(blob); }, outputType, quality ); }); }
Использование:
const compressedAvatar = await compressImageFile(file, { maxWidth: 256, maxHeight: 256, quality: 0.9, outputType: 'image/jpeg', }); const formData = new FormData(); formData.append('avatar', compressedAvatar, 'avatar.jpg'); await fetch('/api/avatar', { method: 'POST', body: formData, });
Это ускоряет загрузку файлов, уменьшает расходы на хранение данных и делает интерфейс более отзывчивым.
❯ Создание предпросмотра файлов через Blob URL
Blob URL значительно упрощают локальный предпросмотр файлов.
Чтобы показать файл пользователю, необязательно сначала загружать его на сервер. Браузер может создать временный объектный URL и использовать его как источник для изображения, видео, аудио или ссылки.
Предпросмотр изображения:
function previewImage(file: File, image: HTMLImageElement) { const url = URL.createObjectURL(file); image.onload = () => { // Отзываем ссылку только после полной загрузки изображения URL.revokeObjectURL(url); }; image.src = url; }
Предпросмотр видео:
function previewVideo(file: File, video: HTMLVideoElement) { const url = URL.createObjectURL(file); // Прим. пер.: не очень надежный отзыв ссылки, // иногда такое видео может не воспроизводиться полностью video.onloadedmetadata = () => { URL.revokeObjectURL(url); }; video.src = url; }
Предпросмотр аудио:
function previewAudio(file: File, audio: HTMLAudioElement) { const url = URL.createObjectURL(file); audio.onloadedmetadata = () => { URL.revokeObjectURL(url); }; audio.src = url; }
Для предпросмотра текста нужен другой подход, потому что текст необходимо сначала прочитать:
async function previewText(file: File, container: HTMLElement) { const text = await file.text(); const pre = document.createElement('pre'); pre.textContent = text.length > 10_000 // Обрезаем текст после 10_000 символов и добавляем ... ? `${text.slice(0, 10_000)}\n\n...preview truncated` : text; container.replaceChildren(pre); }
В реальном приложении логику предпросмотра лучше вынести в отдельный класс:
class FilePreviewer { constructor(private container: HTMLElement) {} async preview(file: File) { this.container.replaceChildren(); if (file.type.startsWith('image/')) { return this.renderImage(file); } if (file.type.startsWith('video/')) { return this.renderVideo(file); } if (file.type.startsWith('audio/')) { return this.renderAudio(file); } if (file.type.startsWith('text/') || file.type === 'application/json') { return this.renderText(file); } return this.renderUnsupported(file); } private renderImage(file: File) { const image = document.createElement('img'); image.style.maxWidth = '100%'; const url = URL.createObjectURL(file); image.onload = () => { URL.revokeObjectURL(url); }; image.src = url; this.container.appendChild(image); } private renderVideo(file: File) { const video = document.createElement('video'); video.controls = true; video.style.maxWidth = '100%'; const url = URL.createObjectURL(file); video.onloadedmetadata = () => { URL.revokeObjectURL(url); }; video.src = url; this.container.appendChild(video); } private renderAudio(file: File) { const audio = document.createElement('audio'); audio.controls = true; const url = URL.createObjectURL(file); audio.onloadedmetadata = () => { URL.revokeObjectURL(url); }; audio.src = url; this.container.appendChild(audio); } private async renderText(file: File) { const text = await file.text(); const pre = document.createElement('pre'); pre.textContent = text.length > 10_000 ? `${text.slice(0, 10_000)}\n\n...preview truncated` : text; this.container.appendChild(pre); } private renderUnsupported(file: File) { const message = document.createElement('p'); message.textContent = `Preview is not available for ${file.name}.`; this.container.appendChild(message); } }
Так логика предпросмотра файлов остается централизованной, а поддерживать такой код становится проще.
❯ Экспорт CSV, JSON и текстовых файлов
Blob также отлично подходит для реализации функций экспорта файлов.
Базовая вспомогательная функция для скачивания может выглядеть так:
function triggerBlobDownload(blob: Blob, filename: string) { const url = URL.createObjectURL(blob); const link = document.createElement('a'); link.href = url; link.download = filename; link.style.display = 'none'; document.body.appendChild(link); link.click(); document.body.removeChild(link); setTimeout(() => { URL.revokeObjectURL(url); }, 0); }
Экспорт JSON
function exportJson(data: unknown, filename = 'data.json') { const blob = new Blob( [JSON.stringify(data, null, 2)], { type: 'application/json' } ); triggerBlobDownload(blob, filename); }
Экспорт CSV
При генерации CSV важно правильно экранировать данные. Необходимо корректно обрабатывать запятые, кавычки и переносы строк.
function escapeCsvCell(value: unknown) { const text = String(value ?? ''); if ( text.includes(',') || text.includes('"') || text.includes('\n') ) { return `"${text.replace(/"/g, '""')}"`; } return text; } function exportCsv( rows: Record<string, unknown>[], filename = 'report.csv' ) { if (rows.length === 0) { triggerBlobDownload( new Blob([''], { type: 'text/csv;charset=utf-8' }), filename ); return; } const headers = Object.keys(rows[0]); const csv = [ headers.map(escapeCsvCell).join(','), ...rows.map(row => headers .map(header => escapeCsvCell(row[header])) .join(',') ), ].join('\n'); const bom = '\uFEFF'; const blob = new Blob([bom + csv], { type: 'text/csv;charset=utf-8', }); triggerBlobDownload(blob, filename); }
Использование:
exportCsv( [ { date: '2026-05-19', product: 'Pro Plan', revenue: 120 }, { date: '2026-05-20', product: 'Team Plan', revenue: 340 }, ], 'sales-report.csv' );
Добавление bom (Byte Order Mark - маркер последовательности байтов) помогает табличным приложениям корректно определять UTF-8, особенно если файл содержит текст не на английском языке.
❯ Утечки памяти из-за Blob URL
Blob URL — мощный инструмент, однако работа с ним все же требует дополнительных ресурсов.
Каждый вызов URL.createObjectURL() создает ссылку, которой управляет браузер.
Если никогда не вызывать URL.revokeObjectURL(), эти ссылки могут оставаться в памяти дольше, чем нужно.
Плохой пример:
function renderPreview(file: File) { const url = URL.createObjectURL(file); const image = document.createElement('img'); image.src = url; document.body.appendChild(image); // URL никогда не очищается }
Для одного изображения это может быть незаметно. Но в долго работающем приложении, где пользователи просматривают десятки или сотни файлов, проблема становится серьезной.
❯ Система учета Blob URL
Для приложений, активно работающих с Blob, особенно важно централизованное управление памятью:
class BlobUrlRegistry { private urls = new Set<string>(); create(blob: Blob) { const url = URL.createObjectURL(blob); this.urls.add(url); return url; } revoke(url: string) { URL.revokeObjectURL(url); this.urls.delete(url); } revokeAll() { for (const url of this.urls) { URL.revokeObjectURL(url); } this.urls.clear(); } get size() { return this.urls.size; } }
Использование:
const registry = new BlobUrlRegistry(); const previewUrl = registry.create(file); image.src = previewUrl; image.onload = () => { registry.revoke(previewUrl); };
Перед закрытием страницы или удалением компонента:
registry.revokeAll();
Этот подход особенно полезен в:
редакторах изображений;
менеджерах загрузок;
файловых дашбордах;
инструментах для дизайна;
долго работающих SPA-приложениях.
❯ Пример очистки на уровне компонента
Ниже показан простой компонент для предпросмотра файлов:
class SafeImagePreview { private currentUrl: string | null = null; constructor( private container: HTMLElement, private registry: BlobUrlRegistry ) {} show(file: File) { this.cleanup(); const image = document.createElement('img'); image.style.maxWidth = '100%'; const url = this.registry.create(file); this.currentUrl = url; image.src = url; image.onerror = () => { this.cleanup(); }; this.container.replaceChildren(image); } cleanup() { if (this.currentUrl) { this.registry.revoke(this.currentUrl); this.currentUrl = null; } this.container.replaceChildren(); } }
Суть подхода в том, что компонент сам отвечает за жизненный цикл своего Blob URL и освобождает его при необходимости.
❯ Практические рекомендации
С Blob довольно легко работать, но соблюдение нескольких правил может заметно повысить качество и стабильность приложения.
Всегда указываем MIME-тип
Избегаем такого подхода:
new Blob([data]);
Делаем так:
new Blob([data], { type: 'application/json', });
Корректно указанный MIME-тип удобен для всех: браузера, сервера и самого пользователя.
Не используем URL данных для большого объема контента
URL данных хорошо подходят для небольших примеров, но при работе с крупными экспортами и сгенерированными файлами Blob URL обычно работают эффективнее.
Используем загрузку по частям для больших файлов
Не стоит считывать большие файлы целиком, особенно, если их размер заранее неизвестен.
Вместо этого используем:
file.slice(start, end);
чтобы обрабатывать файлы постепенно, а не целиком.
Сжимаем изображения перед загрузкой
В большинстве случаев пользователям не нужен аватар размером 6 МБ. Сжатие изображений на стороне клиента позволяет ускорить загрузку файлов и уменьшить нагрузку на сервер.
Отзываем объектные URL
У каждого createObjectURL() должен быть план очистки ресурсов.
Централизуем очистку в сложных приложениях
Если приложение создает много предпросмотров, сгенерированных файлов для скачивания или состояний истории редактирования, используем реестр или отдельный менеджер для отслеживания URL.
❯ Когда Blob подходит лучше всего
Blob отлично подходит, если нам нужно:
генерировать файлы прямо в браузере;
отображать локальный предпросмотр файлов перед загрузкой;
загружать бинарный контент;
обрабатывать большие файлы небольшими частями;
сжимать изображения;
экспортировать CSV, JSON или текстовые файлы;
работать с бинарными данными без постоянных запросов к серверу.
Но Blob — не универсальное решение для любой обработки файлов. Тяжелое редактирование видео, трансформация огромных наборов данных или сложная обработка документов все еще могут требовать серверной инфраструктуры или использования веб-воркеров.
Тем не менее, Blob отлично подходит для решения многих задач фронтенда.
❯ Заключение
Blob API кажется довольно простым, но именно он лежит в основе многих современных механизмов работы с файлами в браузере.
Он используется для загрузок, предпросмотра, скачивания, экспорта, обработки медиаконтента, работы с бинарными данными и управления памятью.
Самое важное здесь — не просто научиться создавать Blob. Настоящий навык заключается в понимании жизненного цикла данных и правильном управлении им.
Создаем данные осознанно.
Используем корректные MIME-типы.
Обрабатываем большие файлы по частям.
Используем Blob URL вместо крупных URL данных.
Освобождаем объектные URL после использования.
Когда эти привычки становятся частью рабочего процесса, работа с файлами во фронтенде становится быстрее, безопаснее и намного проще в поддержке.
Новости, обзоры продуктов и конкурсы от команды Timeweb.Cloud — в нашем Telegram-канале ↩

