Last active
October 1, 2022 09:22
-
-
Save DylanPiercey/6f945d4f75ae310c4173d5281f2f2c81 to your computer and use it in GitHub Desktop.
Progressive HTML with fetch
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 getWritableDOM = (() => { | |
const testDoc = document.implementation.createHTMLDocument(); | |
testDoc.write("<script>"); | |
// Safari and potentially other browsers strip script tags from detached documents. | |
// If that's the case we'll fallback to an iframe implementation. | |
if (testDoc.scripts.length) { | |
return target => | |
toWritable(target, document.implementation.createHTMLDocument()); | |
} else { | |
return target => { | |
const frame = document.createElement("iframe"); | |
frame.src = ""; | |
frame.style.display = "none"; | |
target.appendChild(frame); | |
return toWritable(target, frame.contentDocument, () => frame.remove()); | |
}; | |
} | |
function toWritable(target, doc, onClose) { | |
doc.write("<!DOCTYPE html><body><template>"); | |
const root = doc.body.firstElementChild.content; | |
const walker = doc.createTreeWalker(root); | |
const targetNodes = new WeakMap([[root, target]]); | |
let isBlocked = false; | |
let isClosed = false; | |
let pendingText; | |
let scanNode; | |
return { | |
close, | |
write(chunk) { | |
doc.write(chunk); | |
walk(); | |
} | |
}; | |
function walk() { | |
let node; | |
if (isBlocked) { | |
// If we are blocked, we walk ahead and preload | |
// any assets we can ahead of the last checked node. | |
const blockedNode = walker.currentNode; | |
if (scanNode) walker.currentNode = scanNode; | |
while ((node = walker.nextNode())) { | |
const link = getPreloadLink((scanNode = node)); | |
if (link) { | |
link.onload = link.onerror = () => link.remove(); | |
target.insertBefore(link, target.firstChild); | |
} | |
} | |
walker.currentNode = blockedNode; | |
} else { | |
while ((node = walker.nextNode())) { | |
if (pendingText) { | |
// The final text content must be added lazily since it can be in an incomplete state. | |
targetNodes | |
.get(pendingText.parentNode) | |
.appendChild(document.importNode(pendingText, false)); | |
pendingText = null; | |
} | |
if (node.nodeType === Node.TEXT_NODE) { | |
pendingText = node; | |
continue; | |
} | |
const clone = document.importNode(node, false); | |
if (isBlocking(clone)) { | |
isBlocked = true; | |
clone.onload = clone.onerror = () => { | |
isBlocked = false; | |
walk(); // Continue the normal content injecting walk. | |
}; | |
} | |
targetNodes.set(node, clone); | |
targetNodes.get(node.parentNode).appendChild(clone); | |
if (isBlocked) return walk(); // Start walking for preloads. | |
} | |
if (isClosed) close(); // Some blocking content prevented close, now we can close. | |
} | |
} | |
function close() { | |
isClosed = true; | |
if (!isBlocked) { | |
if (pendingText) { | |
// The final text content must be added lazily since it can be in an incomplete state. | |
targetNodes | |
.get(pendingText.parentNode) | |
.appendChild(document.importNode(pendingText, false)); | |
} | |
doc.close(); | |
if (onClose) onClose(); | |
} | |
} | |
} | |
function isBlocking(node) { | |
return ( | |
node.nodeType === Node.ELEMENT_NODE && | |
((node.tagName === "SCRIPT" && | |
node.src && | |
!( | |
node.noModule || | |
node.type === "module" || | |
node.hasAttribute("async") || | |
node.hasAttribute("defer") | |
)) || | |
(node.tagName === "LINK" && | |
node.rel === "stylesheet" && | |
(!node.media || matchMedia(node.media).matches))) | |
); | |
} | |
function getPreloadLink(node) { | |
if (node.nodeType === Node.ELEMENT_NODE) { | |
let link; | |
switch (node.tagName) { | |
case "SCRIPT": | |
if (node.src && !node.noModule) { | |
link = document.createElement("link"); | |
link.href = node.src; | |
if (node.module) { | |
link.rel = "modulepreload"; | |
} else { | |
link.rel = "preload"; | |
link.as = "script"; | |
} | |
} | |
break; | |
case "LINK": | |
if ( | |
node.rel === "stylesheet" && | |
(!node.media || matchMedia(node.media).matches) | |
) { | |
link = document.createElement("link"); | |
link.href = node.href; | |
link.rel = "preload"; | |
link.as = "style"; | |
} | |
break; | |
case "IMG": | |
link = document.createElement("link"); | |
link.rel = "preload"; | |
link.as = "image"; | |
if (node.srcset) { | |
link.imageSrcset = node.srcset; | |
link.imageSizes = node.sizes; | |
} else { | |
link.href = node.src; | |
} | |
break; | |
} | |
if (link) { | |
if (node.integrity) { | |
link.integrity = node.integrity; | |
} | |
if (node.crossOrigin) { | |
link.crossOrigin = node.crossOrigin; | |
} | |
return link; | |
} | |
} | |
} | |
})(); | |
// Example usage. | |
async function streamInto(target, url) { | |
const res = await fetch(url); | |
const decoder = new TextDecoder(); | |
const reader = res.body.getReader(); | |
const writable = getWritableDOM(target); | |
try { | |
let value; | |
while (!({ value } = await reader.read()).done) { | |
writable.write(decoder.decode(value)); | |
} | |
} finally { | |
writable.close(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Usage: