From b40354235a34a72312ddd65e6caaeba1e7bac7b6 Mon Sep 17 00:00:00 2001 From: Adrian Vollmer Date: Sat, 27 Apr 2024 15:44:16 +0200 Subject: [PATCH] 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. --- zundler/assets/inject_pre.js | 75 +++++++++++++++++++++----------- zundler/assets/zundler_common.js | 30 ------------- zundler/assets/zundler_main.js | 71 +++++++++++++++++++++++++++++- 3 files changed, 119 insertions(+), 57 deletions(-) diff --git a/zundler/assets/inject_pre.js b/zundler/assets/inject_pre.js index daae3e8..8cb4779 100644 --- a/zundler/assets/inject_pre.js +++ b/zundler/assets/inject_pre.js @@ -66,37 +66,21 @@ window.history.replaceState = myReplaceState; const { fetch: originalFetch } = window; -async function waitFor(predicate, timeout) { - return new Promise((resolve, reject) => { - const check = () => { - if (!predicate()) return; - clearInterval(interval); - resolve(); - }; - const interval = setInterval(check, 100); - check(); - - if (!timeout) return; - setTimeout(() => { - clearInterval(interval); - reject(); - }, timeout); - }); +function waitForParentResponse(path) { + return new Promise((resolve, reject) => { + retrieveFileFromParent(path, file => { + resolve(file); + }); + }); } -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; var path = normalizePath(resource); var response; if (isVirtual(path)) { - var file = retrieveFile(path); + var file = await waitForParentResponse(path); var data = file.data; if (file.base64encoded) { 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) => { // console.log("Fix mutated elements...", mutationList); mutationList.forEach((mutation) => { @@ -118,7 +143,7 @@ const observer = new MutationObserver((mutationList) => { fixLink(a); }); Array.from(mutation.target.querySelectorAll("img")).forEach( img => { - embedImg(img); + embedImgFromParent(img); }); Array.from(mutation.target.querySelectorAll("form")).forEach( form => { fixForm(form); diff --git a/zundler/assets/zundler_common.js b/zundler/assets/zundler_common.js index 0eb5fbc..671d2c7 100644 --- a/zundler/assets/zundler_common.js +++ b/zundler/assets/zundler_common.js @@ -2,19 +2,6 @@ * 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) { // Return true if the url should be retrieved from the virtual file tree 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) { // make relative paths absolute var result = window.globalContext.current_path; diff --git a/zundler/assets/zundler_main.js b/zundler/assets/zundler_main.js index c7fca18..0284e45 100644 --- a/zundler/assets/zundler_main.js +++ b/zundler/assets/zundler_main.js @@ -1,5 +1,18 @@ 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) { if (!href) {return;} 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) { function unicodeToBase64(string) { const utf8EncodedString = unescape(encodeURIComponent(string)); @@ -41,12 +73,18 @@ var prepare = function(html) { var parser = new DOMParser(); 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 // problematic characters: `, ", ', &, , ... // atob is insufficient, because it only deals with ASCII - we have // 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 = ` 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) { Array.from(doc.querySelectorAll("script")).forEach( oldScript => { const newScript = doc.createElement("script"); @@ -181,8 +236,20 @@ window.onload = function() { if (evnt.data.action == 'ready') { 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') { showMenu(); + } else if (evnt.data.action == 'set_title') { // iframe has finished loading and sent us its title // parent sets the title and responds with the globalContext object