Streaming HTML and DOM Comparison Algorithms

0

In recent years, web developers have started utilizing techniques that involve streaming HTML and JavaScript. This article explores the benefits of streaming HTML and how it can be used to maximize performance. Let’s look at specific examples of how streaming technology enhances website performance.

pixabay

Basic Principles of Streaming HTML

Streaming HTML is a method where the browser processes HTML chunks as they are received during the initial loading. This can reduce page load time and improve user experience.

Header Settings

To enable streaming from the server, the following headers need to be set:

{
  "transfer-encoding": "chunked",
  "vary": "Accept-Encoding",
  "content-type": "text/html; charset=utf-8"
}

The response uses ReadableStream. For example, when using Bun, it can be set up as follows:

const encoder = new TextEncoder();

return new Response(
  new ReadableStream({
    start(controller) {
      controller.enqueue(encoder.encode('<html lang="en">'));
      controller.enqueue(encoder.encode('<head />'));
      controller.enqueue(encoder.encode('<body>'));
      controller.enqueue(encoder.encode('<div class="foo">Bar</div>'));
      controller.enqueue(encoder.encode('</body>'));
      controller.enqueue(encoder.encode('</html>'));
      controller.close();
    },
  })
);

The browser can execute small JS scripts during streaming to dynamically change HTML content.

Changing HTML Content During Streaming

One of the main advantages of streaming HTML is the ability to dynamically change HTML content. React Suspense is a prime example of this approach. It shows placeholders while loading the rest of the HTML, and later loads the missing content to provide a better user experience.

return new Response(
  new ReadableStream({
    async start(controller) {
      const suspensePromises = [];
      controller.enqueue(encoder.encode('<html lang="en">'));
      controller.enqueue(encoder.encode('<head>'));
      controller.enqueue(encoder.encode('<script src="unsuspense.js"></script>'));
      controller.enqueue(encoder.encode('</head>'));
      controller.enqueue(encoder.encode('<body>'));
      controller.enqueue(encoder.encode('<div id="suspensed:1">Loading...</div>'));
      suspensePromises.push(
        computeExpensiveChunk().then((content) => {
          controller.enqueue(encoder.encode(`<template id="suspensed-content:1">${content}</template>`));
          controller.enqueue(encoder.encode(`<script>unsuspense('1')</script>`));
        })
      );
      controller.enqueue(encoder.encode('<div class="foo">Bar</div>'));
      controller.enqueue(encoder.encode('</body>'));
      controller.enqueue(encoder.encode('</html>'));
      await Promise.all(suspensePromises);
      controller.close();
    },
  })
);

The unsuspense.js file executes during streaming via window.unsuspense, replacing placeholders with actual content.

Real-Time Navigation and RPC

Since 2023, Chrome has supported the View Transition API. This API allows for smooth transition effects similar to single-page applications (SPAs) while updating DOM content all at once.

window.navigation.addEventListener('navigate', navigate);

function navigate(event) {
  const url = new URL(event.destination.url);
  const decoder = new TextDecoder();
  if (location.origin !== url.origin) return;
  event.intercept({
    async handler() {
      const res = await fetch(url.pathname);
      const doc = document.implementation.createHTMLDocument();
      const stream = res.body.getReader();
      await document.startViewTransition(() => {
        document.documentElement.replaceWith(doc.documentElement);
        document.documentElement.scrollTop = 0;
      }).ready;
      while (true) {
        const { done, value } = await stream.read();
        if (done) break;
        await document.startViewTransition(() => doc.write(decoder.decode(value))).ready;
      }
    },
  });
}

DOM Comparison Algorithms

DOM comparison algorithms help browsers update the DOM in real-time while streaming HTML. Several open-source libraries offer these capabilities.

  • morphdom
  • set-dom
  • diffhtml
  • diffDOM
  • nanomorph
  • incremental-dom

These libraries primarily use breadth-first search (BFS) to traverse and update the DOM tree. However, during streaming, HTML nodes arrive in depth-first search (DFS) order. The parse-html-stream library can be used to handle this.

import parseHTMLStream from 'parse-html-stream';

window.navigation.addEventListener('navigate', navigate);

function navigate(event) {
  const url = new URL(event.destination.url);
  const decoder = new TextDecoder();
  if (location.origin !== url.origin) return;
  event.intercept({
    async handler() {
      const res = await fetch(url.pathname);
      const doc = document.implementation.createHTMLDocument();
      const stream = res.body.getReader();
      await document.startViewTransition(() => {
        document.documentElement.replaceWith(doc.documentElement);
        document.documentElement.scrollTop = 0;
      }).ready;
      for await (const node of parseHTMLStream(reader)) {
        console.log(node.nodeName);
      }
    },
  });
}

Conclusion

Streaming HTML and DOM comparison algorithms are illuminating the future of web development. By leveraging these technologies, developers can reduce initial loading times and greatly enhance user experience. Furthermore, they enable dynamic updates to HTML content without complex additional requests.

References

Leave a Reply