Skip to content

Instantly share code, notes, and snippets.

@DylanPiercey
Created October 31, 2019 17:36
Show Gist options
  • Save DylanPiercey/3f123a491e57f8dba8a8be97de726552 to your computer and use it in GitHub Desktop.
Save DylanPiercey/3f123a491e57f8dba8a8be97de726552 to your computer and use it in GitHub Desktop.
Progressive HTML with AJAX
const END_MARKER = `#${Math.random()}`;
const END_COMMENT = `<!${END_MARKER}>`;
function streamInto(container, url) {
const doc = document.implementation.createHTMLDocument(""); // Using a detached document as a streaming html parser.
const xhr = new XMLHttpRequest();
let pos = 0;
doc.write("<body>"); // We don't expect a head, so we flush the body to begin parsing in that context.
const streamRoot = document.adoptNode(doc.body); // We adopt the body into the main document, this overrides the document for the subsequent writes which flags scripts as executable in the main document.
// We use a walker to gradually move content over from the detached document into the real document.
const walker = document.createTreeWalker(streamRoot);
streamRoot.related = container;
xhr.onload = () => doc.close();
xhr.onprogress = () => {
doc.write(xhr.response.slice(pos) + END_COMMENT); // We add an end comment to each flush so we can track what the last unclosed element was.
pos = xhr.response.length;
while (curNode = walker.nextNode()) {
if (curNode.nodeType === Node.COMMENT_NODE && curNode.data === END_MARKER) {
// We read until the end marker is found.
break;
}
const parentNode = curNode.parentNode.related; // `related` is either going to be the stream container, or a shallow clone for an unclosed node below.
const isIncomplete = !curNode.nextSibling;
if (isIncomplete) {
// When there is no nextSibling at current position that means this node is not closed.
// Otherwise the nextSibling would be the end marker, or some other content.
// In this case we shallow clone the node so that we can put the clone in the real document.
// We will still continue writing to the original node in the detached document.
const clone = curNode.cloneNode();
curNode.related = clone;
curNode = clone;
} else {
// If we had a nextSibling then we know the node has closed.
// We skip walking into that subtree with this walker and just move the existing node
// into the real document below.
walker.nextSibling();
}
parentNode.appendChild(curNode);
if (!isIncomplete) {
walker.previousNode();
}
}
};
xhr.responseType = "text";
xhr.open("GET", url);
xhr.send();
}
streamInto(document.getElementById("container"), "http://some-streaming-site.com");
@tigt
Copy link

tigt commented Dec 6, 2019

The best I’ve got for reinstating render-blocking stylesheets would be reverting to Jake’s original <streaming-element> wrapper, and then including CSS like the following:

streaming-element link[rel=stylesheet]:not(.loaded) ~ * {
  display: none !important;
}

Then, something like:

const observer = new MutationObserver(loadChecker)
observer.observe(
  document.querySelector('streaming-element'),
  { childList: true, subtree: true } // can also watch for `characterData` if worried about <style>@import
)

function loadChecker (mutations) {
  mutations.forEach(mutation => {
    mutation.addedNodes.filter(node => node.nodeName === 'link' && node.rel === 'stylesheet')
      .forEach(link => {
        if (link.styleSheet) { // already loaded
          link.classList.add('loaded')
        } else {
          link.onload = function() { this.classList.add('loaded') }
        }
      })
  })
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment