Improve performance by using messages

Instead of inserting the entire global context into the iframe's HTML,
where we need to encode it to and decode it from JSON and base64, we
only insert a stripped global context, i.e. without the file tree and
the utils. If these are needed in the iframe, for example when a script
changes the DOM and needs to load an image, we use messages to
communicate with the parent document and retrieve the file in question.
This commit is contained in:
Adrian Vollmer 2024-04-27 15:44:16 +02:00
parent c2373c6493
commit b40354235a
3 changed files with 119 additions and 57 deletions

View File

@ -66,37 +66,21 @@ window.history.replaceState = myReplaceState;
const { fetch: originalFetch } = window; const { fetch: originalFetch } = window;
async function waitFor(predicate, timeout) { function waitForParentResponse(path) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const check = () => { retrieveFileFromParent(path, file => {
if (!predicate()) return; resolve(file);
clearInterval(interval); });
resolve(); });
};
const interval = setInterval(check, 100);
check();
if (!timeout) return;
setTimeout(() => {
clearInterval(interval);
reject();
}, timeout);
});
} }
window.fetch = async (...args) => {
// wait until globalContext is ready
try {
await waitFor(() => window.hasOwnProperty("globalContext"), 10000);
} catch (err) {
throw err;
}
window.fetch = async (...args) => {
let [resource, config ] = args; let [resource, config ] = args;
var path = normalizePath(resource); var path = normalizePath(resource);
var response; var response;
if (isVirtual(path)) { if (isVirtual(path)) {
var file = retrieveFile(path); var file = await waitForParentResponse(path);
var data = file.data; var data = file.data;
if (file.base64encoded) { if (file.base64encoded) {
data = _base64ToArrayBuffer(data); data = _base64ToArrayBuffer(data);
@ -110,6 +94,47 @@ window.fetch = async (...args) => {
}; };
var retrieveFileFromParent = function(path, callback) {
// Get the file into the iframe by messaging the parent document
// console.log("Retrieving file from parent: " + path);
function messageHandler(event) {
if (event.data.action === "sendFile" && event.data.argument.path === path) {
callback(event.data.argument.file);
window.removeEventListener('message', messageHandler);
}
}
window.addEventListener('message', messageHandler);
window.parent.postMessage({
action: "retrieveFile",
argument: {
path: path,
}
}, '*');
};
var embedImgFromParent = function(img) {
function setSrc(img, file) {
if (mime_type == 'image/svg+xml') {
img.setAttribute('src', "data:image/svg+xml;charset=utf-8;base64, " + btoa(file.data));
} else {
img.setAttribute('src', `data:${file.mime_type};base64, ${file.data}`);
}
};
if (img.hasAttribute('src')) {
const src = img.getAttribute('src');
if (isVirtual(src)) {
var path = normalizePath(src);
retrieveFileFromParent(path, file => setSrc(img, file));
};
};
};
const observer = new MutationObserver((mutationList) => { const observer = new MutationObserver((mutationList) => {
// console.log("Fix mutated elements...", mutationList); // console.log("Fix mutated elements...", mutationList);
mutationList.forEach((mutation) => { mutationList.forEach((mutation) => {
@ -118,7 +143,7 @@ const observer = new MutationObserver((mutationList) => {
fixLink(a); fixLink(a);
}); });
Array.from(mutation.target.querySelectorAll("img")).forEach( img => { Array.from(mutation.target.querySelectorAll("img")).forEach( img => {
embedImg(img); embedImgFromParent(img);
}); });
Array.from(mutation.target.querySelectorAll("form")).forEach( form => { Array.from(mutation.target.querySelectorAll("form")).forEach( form => {
fixForm(form); fixForm(form);

View File

@ -2,19 +2,6 @@
* Functions that will be needed by several files * Functions that will be needed by several files
*/ */
var retrieveFile = function(path) {
// console.log("Retrieving file: " + path);
var fileTree = window.globalContext.fileTree;
var file = fileTree[path];
if (!file) {
console.warn("File not found: " + path);
return "";
} else {
return file;
}
};
var isVirtual = function(url) { var isVirtual = function(url) {
// Return true if the url should be retrieved from the virtual file tree // Return true if the url should be retrieved from the virtual file tree
var _url = url.toString().toLowerCase(); var _url = url.toString().toLowerCase();
@ -71,23 +58,6 @@ var fixForm = function(form) {
}; };
var embedImg = function(img) {
if (img.hasAttribute('src')) {
const src = img.getAttribute('src');
if (isVirtual(src)) {
var path = normalizePath(src);
const file = retrieveFile(path);
const mime_type = file.mime_type;
if (mime_type == 'image/svg+xml') {
img.setAttribute('src', "data:image/svg+xml;charset=utf-8;base64, " + btoa(file.data));
} else {
img.setAttribute('src', `data:${mime_type};base64, ${file.data}`);
}
};
};
};
var normalizePath = function(path) { var normalizePath = function(path) {
// make relative paths absolute // make relative paths absolute
var result = window.globalContext.current_path; var result = window.globalContext.current_path;

View File

@ -1,5 +1,18 @@
const iFrameId = 'zundler-iframe'; const iFrameId = 'zundler-iframe';
var retrieveFile = function(path) {
// console.log("Retrieving file: " + path);
var fileTree = window.globalContext.fileTree;
var file = fileTree[path];
if (!file) {
console.warn("File not found: " + path);
return "";
} else {
return file;
}
};
var setFavicon = function(href) { var setFavicon = function(href) {
if (!href) {return;} if (!href) {return;}
var favicon = document.createElement("link"); var favicon = document.createElement("link");
@ -32,6 +45,25 @@ var createIframe = function() {
} }
function deepCopyExcept(obj, skipProps) {
if (obj === null || typeof obj !== 'object') {
return obj;
}
let result = Array.isArray(obj) ? [] : {};
for (let key in obj) {
if (obj.hasOwnProperty(key)) {
if (!skipProps.includes(key)) {
result[key] = deepCopyExcept(obj[key], skipProps);
}
}
}
return result;
}
var prepare = function(html) { var prepare = function(html) {
function unicodeToBase64(string) { function unicodeToBase64(string) {
const utf8EncodedString = unescape(encodeURIComponent(string)); const utf8EncodedString = unescape(encodeURIComponent(string));
@ -41,12 +73,18 @@ var prepare = function(html) {
var parser = new DOMParser(); var parser = new DOMParser();
var doc = parser.parseFromString(html, "text/html"); var doc = parser.parseFromString(html, "text/html");
const gcTag = doc.createElement("script"); // Insert the global context into the iframe's DOM, but without the file
// tree or utils. They are not necessary; the iframe will message the
// parent document to retrieve files.
//
// Convert JSON object to b64 because it contain all kinds of // Convert JSON object to b64 because it contain all kinds of
// problematic characters: `, ", ', &, </script>, ... // problematic characters: `, ", ', &, </script>, ...
// atob is insufficient, because it only deals with ASCII - we have // atob is insufficient, because it only deals with ASCII - we have
// unicode // unicode
var serializedGC = unicodeToBase64(JSON.stringify(window.globalContext)); const gcTag = doc.createElement("script");
const strippedGC = deepCopyExcept(window.globalContext, ["fileTree", "utils"]);
var serializedGC = unicodeToBase64(JSON.stringify(strippedGC));
gcTag.textContent = ` gcTag.textContent = `
function base64ToUnicode(base64String) { function base64ToUnicode(base64String) {
@ -82,6 +120,23 @@ var prepare = function(html) {
} }
var embedImg = function(img) {
if (img.hasAttribute('src')) {
const src = img.getAttribute('src');
if (isVirtual(src)) {
var path = normalizePath(src);
const file = retrieveFile(path);
const mime_type = file.mime_type;
if (mime_type == 'image/svg+xml') {
img.setAttribute('src', "data:image/svg+xml;charset=utf-8;base64, " + btoa(file.data));
} else {
img.setAttribute('src', `data:${mime_type};base64, ${file.data}`);
}
};
};
};
var embedJs = function(doc) { var embedJs = function(doc) {
Array.from(doc.querySelectorAll("script")).forEach( oldScript => { Array.from(doc.querySelectorAll("script")).forEach( oldScript => {
const newScript = doc.createElement("script"); const newScript = doc.createElement("script");
@ -181,8 +236,20 @@ window.onload = function() {
if (evnt.data.action == 'ready') { if (evnt.data.action == 'ready') {
hideLoadingIndicator(); hideLoadingIndicator();
} else if (evnt.data.action == 'retrieveFile') {
const path = evnt.data.argument.path;
const file = retrieveFile(path);
iframe.contentWindow.postMessage({
action: "sendFile",
argument: {
path: path,
file: file,
},
}, "*");
} else if (evnt.data.action == 'showMenu') { } else if (evnt.data.action == 'showMenu') {
showMenu(); showMenu();
} else if (evnt.data.action == 'set_title') { } else if (evnt.data.action == 'set_title') {
// iframe has finished loading and sent us its title // iframe has finished loading and sent us its title
// parent sets the title and responds with the globalContext object // parent sets the title and responds with the globalContext object