Created
October 31, 2019 17:36
-
-
Save DylanPiercey/3f123a491e57f8dba8a8be97de726552 to your computer and use it in GitHub Desktop.
Progressive HTML with AJAX
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); |
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
The above is inspired by https://jakearchibald.com/2016/fun-hacks-faster-content/ but resolves the script executing issue mentioned in Firefox.
Remaining issues: