Fwio

Stream Response

Typical data flow in the web

HTTP responses typically send the entire payload to the client all at once. This approach can sometimes result in a slow user experience if the data is large or computationally intense.

In a computation --> response --> consume pipeline, being intrinsically too large blocks the response stage, while computationally intense blocks the computation stage. Both cases result in a slow user experience: “The response arrives so late”.

Breaking data to chunks

The Web Streams API enables developers to stream chunks of the payload as they become available, delivering content in an incremental fashion.

Browsers already do this whn receiving media assets — video buffers and play as more of the content downloads, and sometimes you’ll see images display gradually as more is loaded too.

A chunk is a single piece of data that is written to or read from a stream, which could be of any type such as String for text of Uint8Array for binary files. A particular stream can also contain chunks of different types. A chunk will often not be the most atomic unit of data for a given stream; for example, a byte stream might contain chunks consisting of 16KiB Unit8Arrays, instead of single bytes.

Small chunks of data are emitted and processed early by the consumer, instead of buffering it all into memory and process it in one go.

Readable Streams

A ReadableStream represents an underlying source from which data is consumed.

Typically, ReadableStreams effectively operate in one of two modes: flowing(Push sources) and paused(Pull sources).

  • In flowing mode, data is read from the underlying source automatically and provided to an application as quickly as possible using various events. An example is reading from a TCP socket, where data is constantly being pushed from the OS level, at a rate that can be controlled by changing the TCP window size.

  • In paused mode, the application must explicitly call the interface to read chunks of data from the stream. The data may be available synchronously, e.g. if held by ths OS’s in-memory buffers, or asynchronously, e.g. if needs to be read from disk. Examples include a file handle or a fetch() request.

The data is read sequentially in small chunks, while the unread ones are kept track of by an internal queue of the stream.

There is a controller construct that allows the stream to be controlled, for example, to cancel it. Each stream can be accessed by only one reader at a time, we say the stream is locked by the reader. The reader must be canceled before the stream being read by another one.

According to Processing Data Chunks, we create a ReadableStream with TextEncoder.

// TextEncoder objects turn text content
// into streams of UTF-8 characters
const encoder = new TextEncoder()

// Readable stream object, which clients read from
const readableStream = new ReadableStream({
  // Add the stream's content
  start(controller) {
    const text = 'Stream me!'

    // Enqueue the text to the stream
    controller.enqueue(encoder.encode(text))

    // Prevent any writing to the stream
    controller.close()
  },
})

In the example above, we created a ReadableStream that emits a single chunk of data, which is a UTF-encoded Uint8Array object.

Transform the stream’s data chunks

const decoder = new TextDecoder()

const transformStream = new TransformStream({
  transform(chunk, controller) {
    // Decode the chunk, into a `string`
    const text = decoder.decode(chunk)

    // Make the text uppercase,
    // encode it and add it back to the stream
    controller.enqueue(encoder.encode(text.toUpperCase()))
  },
})

Stream the data chunk by chunk

Finally, we stream the data chunk by chunk as a web response.

// Send the streamed response
return new Response(readableStream.pipeThrough(transformStream), {
  headers: {
    'Content-Type': 'text/html; charset=utf-8',
  },
})

Use cases

Here’s some use cases from Streams Standard:

  • Video effects: piping a readable video stream through a transform stream that applies effects in real time.

  • Decompression: piping a file stream through a transform stream that selectively decompress files from a .tgz archive, turing them into img elements as the user scrolls through an image gallery.

  • Image decoding: piping an HTTP response stream through a transform stream that decodes bytes into bitmap data, and then through another transform that translates bitmaps into PNGs. If installed inside the fetch hook of a service worker, this would allow developers to transparently polyfill new image formats.

Reference