A lazy, streaming Blob/File implementation for JavaScript that defers reading file contents until needed, perfect for situations where file contents don’t fit in memory all at once.
Installation
Features
- Deferred Loading - Blob/file contents loaded on demand to minimize memory usage
- Familiar Interface -
LazyBlob and LazyFile implement the same interface as native Blob and File
- Easy Conversion - Convert to native
ReadableStream with .stream(), or to native Blob/File with .toBlob() and .toFile()
- Standard Constructors - Accepts all the same content types as native
Blob() and File() constructors
- Slice Support - Supports
Blob.slice(), even on streaming content
- Runtime Agnostic - Works in Node.js, Bun, Deno, browsers, and edge runtimes
Why You Need This
JavaScript’s File API requires file contents to be supplied up front:
// Native File API: contents must be loaded immediately
let file = new File(['hello world'], 'hello.txt', { type: 'text/plain' })
This isn’t ideal for server environments where you want to stream files without buffering. LazyFile solves this by accepting a LazyContent object that provides content on-demand:
import { LazyFile } from 'remix/lazy-file'
let lazyContent = {
byteLength: 100000,
stream(start, end) {
// Return a ReadableStream of the requested range
},
}
let lazyFile = new LazyFile(lazyContent, 'example.txt', {
type: 'text/plain',
})
// All File functionality works as expected
await lazyFile.arrayBuffer() // Loads content on-demand
API Reference
LazyFile
A lazy File implementation that defers reading content until needed.
class LazyFile implements File {
constructor(
content: LazyContent | BlobPart[],
name: string,
options?: LazyFileOptions
)
}
content
LazyContent | BlobPart[]
required
Lazy content provider or standard blob parts (strings, ArrayBuffers, etc.).
File options (type, lastModified).
LazyFileOptions
MIME type of the file.Default: ''
Last modified timestamp in milliseconds.Default: Date.now()
Properties
The size of the file in bytes.
The MIME type of the file.
The last modified timestamp in milliseconds.
Methods
arrayBuffer
() => Promise<ArrayBuffer>
Reads the entire file into an ArrayBuffer.Loads the entire file into memory. Use .stream() for large files.
Reads the entire file and decodes it as UTF-8 text.
stream
() => ReadableStream<Uint8Array>
Returns a ReadableStream for reading the file content.Preferred method for large files. Does not buffer content.
slice
(start?: number, end?: number, contentType?: string) => LazyBlob
Returns a new LazyBlob containing a subset of the file’s bytes.
Converts to a native Blob (loads content into memory).Loads the entire file into memory.
Converts to a native File (loads content into memory).Loads the entire file into memory.
LazyBlob
A lazy Blob implementation (similar to LazyFile but without filename/lastModified).
class LazyBlob implements Blob {
constructor(
content: LazyContent | BlobPart[],
options?: LazyBlobOptions
)
}
content
LazyContent | BlobPart[]
required
Lazy content provider or standard blob parts.
LazyContent
The content provider interface for lazy loading.
interface LazyContent {
byteLength: number
stream(start: number, end: number): ReadableStream<Uint8Array>
}
The total length of the content in bytes.
stream
(start: number, end: number) => ReadableStream<Uint8Array>
required
Function that returns a stream of data for the specified byte range (inclusive start, exclusive end).
Examples
Basic Usage
import { LazyFile, type LazyContent } from 'remix/lazy-file'
let content: LazyContent = {
byteLength: 100000,
stream(start, end) {
// Read file contents from somewhere
return new ReadableStream({
start(controller) {
let data = 'X'.repeat(100000).slice(start, end)
controller.enqueue(new TextEncoder().encode(data))
controller.close()
},
})
},
}
let lazyFile = new LazyFile(content, 'example.txt', {
type: 'text/plain',
})
await lazyFile.arrayBuffer() // Loads on demand
lazyFile.name // "example.txt"
lazyFile.type // "text/plain"
lazyFile.size // 100000
Streaming Content
Preferred method for large files:
import { openLazyFile } from 'remix/fs'
let lazyFile = openLazyFile('./large-video.mp4')
// Stream the file without buffering
let response = new Response(lazyFile.stream(), {
headers: {
'Content-Type': lazyFile.type,
'Content-Length': String(lazyFile.size),
},
})
Converting to Native File/Blob
For APIs that require a complete File or Blob:
import { openLazyFile } from 'remix/fs'
let lazyFile = openLazyFile('./document.pdf')
// Convert to native File (loads into memory)
let realFile = await lazyFile.toFile()
// Use with FormData
let formData = new FormData()
formData.append('document', realFile)
.toFile() and .toBlob() read the entire file into memory. Only use these for APIs that require a complete File or Blob. Always prefer .stream() when possible.
Slicing Files
Extract portions of a file:
let lazyFile = openLazyFile('./large-file.bin')
// Get first 1 KB
let chunk = lazyFile.slice(0, 1024)
let chunkData = await chunk.arrayBuffer()
// Get last 1 KB
let lastChunk = lazyFile.slice(-1024)
let lastChunkData = await lastChunk.arrayBuffer()
// Range with new content type
let pdfPage = lazyFile.slice(1000, 5000, 'application/pdf')
Custom LazyContent Implementation
Implement custom content sources:
import { LazyFile, type LazyContent } from 'remix/lazy-file'
class DatabaseContent implements LazyContent {
constructor(private id: string, public byteLength: number) {}
stream(start: number, end: number): ReadableStream<Uint8Array> {
return new ReadableStream({
async start(controller) {
// Fetch data from database
let data = await db.getFileChunk(this.id, start, end)
controller.enqueue(new Uint8Array(data))
controller.close()
},
})
}
}
let content = new DatabaseContent('file-123', 50000)
let lazyFile = new LazyFile(content, 'from-db.txt', {
type: 'text/plain',
})
Using with Response
import { openLazyFile } from 'remix/fs'
import { createFileResponse } from 'remix/response/file'
let lazyFile = openLazyFile('./image.jpg')
// Use with createFileResponse for full HTTP semantics
let response = await createFileResponse(lazyFile, request, {
cacheControl: 'public, max-age=3600',
})
Range Requests
LazyFile works seamlessly with range requests:
let lazyFile = openLazyFile('./video.mp4')
// createFileResponse automatically handles range requests
let response = await createFileResponse(lazyFile, request, {
acceptRanges: true,
})
// Returns 206 Partial Content for range requests
Reading as Text
let lazyFile = openLazyFile('./readme.txt')
// Read entire file as text
let content = await lazyFile.text()
console.log(content)
// Or stream and process incrementally
let reader = lazyFile.stream().pipeThrough(new TextDecoderStream()).getReader()
while (true) {
let { done, value } = await reader.read()
if (done) break
console.log(value) // Process text chunks
}
let lazyFile = openLazyFile('./data.json')
// As text
let text = await lazyFile.text()
let json = JSON.parse(text)
// As ArrayBuffer
let buffer = await lazyFile.arrayBuffer()
let view = new DataView(buffer)
// As stream
let stream = lazyFile.stream()
Use Cases
Serving Static Files
import { openLazyFile } from 'remix/fs'
import { createFileResponse } from 'remix/response/file'
async function serveStatic(path: string, request: Request) {
let lazyFile = openLazyFile(path)
return createFileResponse(lazyFile, request, {
cacheControl: 'public, max-age=31536000, immutable',
})
}
File Downloads
import { openLazyFile } from 'remix/fs'
let lazyFile = openLazyFile('./report.pdf')
let response = new Response(lazyFile.stream(), {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': 'attachment; filename="report.pdf"',
'Content-Length': String(lazyFile.size),
},
})
Streaming Large Files
import { openLazyFile } from 'remix/fs'
let lazyFile = openLazyFile('./movie.mp4')
// Stream without buffering
let response = new Response(lazyFile.stream(), {
headers: {
'Content-Type': lazyFile.type,
'Content-Length': String(lazyFile.size),
'Accept-Ranges': 'bytes',
},
})
Processing File Chunks
let lazyFile = openLazyFile('./data.bin')
let chunkSize = 1024 * 1024 // 1 MB chunks
for (let i = 0; i < lazyFile.size; i += chunkSize) {
let chunk = lazyFile.slice(i, i + chunkSize)
let data = await chunk.arrayBuffer()
await processChunk(data)
}
Type Definitions
interface LazyContent {
byteLength: number
stream(start: number, end: number): ReadableStream<Uint8Array>
}
interface LazyBlobOptions {
type?: string
}
interface LazyFileOptions extends LazyBlobOptions {
lastModified?: number
}
class LazyBlob implements Blob {
constructor(content: LazyContent | BlobPart[], options?: LazyBlobOptions)
readonly size: number
readonly type: string
arrayBuffer(): Promise<ArrayBuffer>
text(): Promise<string>
stream(): ReadableStream<Uint8Array>
slice(start?: number, end?: number, contentType?: string): LazyBlob
toBlob(): Promise<Blob>
}
class LazyFile extends LazyBlob implements File {
constructor(
content: LazyContent | BlobPart[],
name: string,
options?: LazyFileOptions
)
readonly name: string
readonly lastModified: number
toFile(): Promise<File>
}
type ByteRange = { start: number; end: number }
function getByteLength(content: LazyContent | BlobPart[]): number
function getIndexes(size: number, start?: number, end?: number): ByteRange
Best Practices
Prefer Streaming
// ✅ Good: Stream large files
let stream = lazyFile.stream()
let response = new Response(stream)
// ❌ Bad: Buffer entire file
let buffer = await lazyFile.arrayBuffer()
let response = new Response(buffer)
Only Use toFile/toBlob When Necessary
// ✅ Good: Use streaming Response
let response = new Response(lazyFile.stream())
// ❌ Bad: Convert to File unnecessarily
let file = await lazyFile.toFile()
let response = new Response(file.stream()) // Extra conversion
Handle Large Files Appropriately
// ✅ Good: Check size before buffering
if (lazyFile.size < 1024 * 1024) {
let text = await lazyFile.text()
} else {
let stream = lazyFile.stream()
// Process stream in chunks
}
Use Slice for Partial Reads
// ✅ Good: Read only what you need
let header = await lazyFile.slice(0, 1024).arrayBuffer()
// ❌ Bad: Read entire file just for header
let all = await lazyFile.arrayBuffer()
let header = all.slice(0, 1024)
- fs - Includes
openLazyFile() for reading files from disk
- file-storage - Storage abstraction that works with LazyFile
- response -
createFileResponse() works seamlessly with LazyFile