Skip to content

Lazy File Tutorial

This tutorial shows you how to create LazyFile and LazyBlob objects that defer reading content until it is needed, enabling memory-efficient file serving and byte-range support.

Prerequisites

  • A Remix V3 project
  • Basic understanding of the web File and Blob APIs

Step 1: Create a LazyFile

A LazyFile takes three arguments: a read function that returns a ReadableStream, a filename, and an options object with type and size.

ts
import { LazyFile } from 'remix/lazy-file'

let file = new LazyFile(
  () => fetch('https://cdn.example.com/report.pdf').then((r) => r.body!),
  'report.pdf',
  { type: 'application/pdf', size: 1_048_576 },
)

// Metadata is available immediately
console.log(file.name) // 'report.pdf'
console.log(file.type) // 'application/pdf'
console.log(file.size) // 1048576

// Content is NOT fetched yet -- no network call has been made

The read function is called only when you access the content through stream(), arrayBuffer(), or text().

Step 2: Use LazyFile from the Filesystem

The fs package creates LazyFile objects from files on disk. This is the most common way to get a LazyFile.

ts
import { openLazyFile } from 'remix/fs'

let file = await openLazyFile('./uploads/photo.jpg')

console.log(file.name)         // 'photo.jpg'
console.log(file.type)         // 'image/jpeg'
console.log(file.size)         // 2097152
console.log(file.lastModified) // 1704067200000

// File content is NOT read from disk yet

Step 3: Serve a LazyFile as a Response

Because LazyFile implements the File interface, you can pass it directly to response helpers.

ts
import { openLazyFile } from 'remix/fs'
import { createFileResponse } from 'remix/response/file'

export async function loader() {
  let file = await openLazyFile('./public/downloads/manual.pdf')
  return createFileResponse(file)
}

The createFileResponse helper reads the file's metadata to set Content-Type, Content-Length, and ETag headers. The file bytes are streamed to the client on demand.

Step 4: Support Byte-Range Requests

LazyFile supports slice(start, end), which returns a LazyBlob that reads only the requested byte range. This is essential for video streaming and resumable downloads.

ts
import { LazyFile } from 'remix/lazy-file'
import { Range } from 'remix/headers'

export async function loader({ request }: { request: Request }) {
  let file = new LazyFile(
    () => fetch('https://cdn.example.com/video.mp4').then((r) => r.body!),
    'video.mp4',
    { type: 'video/mp4', size: 52_428_800 },
  )

  let rangeHeader = request.headers.get('Range')

  if (rangeHeader) {
    let range = Range.parse(rangeHeader)
    let start = range.ranges[0].start
    let end = range.ranges[0].end ?? file.size - 1

    let slice = file.slice(start, end + 1)

    return new Response(slice.stream(), {
      status: 206,
      headers: {
        'Content-Type': file.type,
        'Content-Length': String(slice.size),
        'Content-Range': `bytes ${start}-${end}/${file.size}`,
        'Accept-Ranges': 'bytes',
      },
    })
  }

  return new Response(file.stream(), {
    headers: {
      'Content-Type': file.type,
      'Content-Length': String(file.size),
      'Accept-Ranges': 'bytes',
    },
  })
}

Step 5: Create a LazyBlob

When you do not need a filename, use LazyBlob directly.

ts
import { LazyBlob } from 'remix/lazy-file'

let blob = new LazyBlob(
  () => fetch('https://api.example.com/data.json').then((r) => r.body!),
  { type: 'application/json', size: 4096 },
)

// Use like any Blob
let text = await blob.text()
let buffer = await blob.arrayBuffer()
let stream = blob.stream()

Summary

ConceptWhat You Learned
Deferred readingContent is not loaded until stream(), arrayBuffer(), or text() is called
From diskUse openLazyFile from remix/fs to create lazy files from the filesystem
ServingPass to createFileResponse for automatic headers and streaming
Byte rangesUse slice(start, end) for range requests and video streaming
LazyBlobUse when you do not need a filename

Next Steps

  • Use fs for filesystem-based lazy files
  • Serve files with response helpers
  • See the API Reference for all constructor options

Released under the MIT License.