Skip to main content
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

npm i remix

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.).
name
string
required
The filename.
options
LazyFileOptions
File options (type, lastModified).

LazyFileOptions

type
string
MIME type of the file.Default: ''
lastModified
number
Last modified timestamp in milliseconds.Default: Date.now()

Properties

name
string
The name of the file.
size
number
The size of the file in bytes.
type
string
The MIME type of the file.
lastModified
number
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.
text
() => Promise<string>
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.
toBlob
() => Promise<Blob>
Converts to a native Blob (loads content into memory).
Loads the entire file into memory.
toFile
() => Promise<File>
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.
options
LazyBlobOptions
Blob options (type).

LazyContent

The content provider interface for lazy loading.
interface LazyContent {
  byteLength: number
  stream(start: number, end: number): ReadableStream<Uint8Array>
}
byteLength
number
required
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
}

Multiple Formats

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

Build docs developers (and LLMs) love