Both in MV2 and MV3 the fastest method of delivering a large Blob is to add an iframe to the web page (exposed in web_accessible_resources), then postMessage from SW through iframe into the content script.
Ownership transfer is instantaneous e.g. a 1GB Blob in 1ms.
There may be existing examples and libraries, try looking.
Here's my quick untested proof-of-concept demo.
- content script usage example
bgFetch(url).then(r => r.blob()).then(blob => { /* do something */ })
bgFetch(url, {headers: {Authorization: 'basic foo'}}).then(.........)
- content script
var reqs, host, shadow, iframe, channel;
/**
* @param {string} url
* @param {RequestInit} [init]
* @return {Promise<Request>}
*/
async function bgFetch(url, init) {
const id = Math.random();
const {stack} = new Error();
const {headers} = init || {};
if (headers instanceof Headers) {
init = {...init, headers: Object.fromEntries(headers)};
}
if (!iframe?.contentWindow) {
await initFrame();
}
return new Promise((resolve, reject) => {
reqs[id] = {resolve, reject, stack};
channel.port1.postMessage({id, url, init});
// TODO: autocreate `transfer` from `init`
});
}
async function initFrame() {
if (reqs) Object.values(reqs).forEach(req => req.reject('iframeKilled'));
reqs = {};
host = document.createElement('div');
shadow = host.attachShadow({mode: 'closed'});
(shadow.adoptedStyleSheets = [new CSSStyleSheet()])
[0].replaceSync(':host { display: none !important }');
iframe = shadow.appendChild(document.createElement('iframe'));
iframe.src = chrome.runtime.getURL('sender.html?id=' + Math.random());
shadow.appendChild(iframe);
(document.body || document.documentElement).appendChild(host);
await (new Promise(resolve => (iframe.onload = resolve)));
channel = new MessageChannel();
channel.port1.onmessage = onFrameMessage;
iframe.contentWindow.postMessage(iframe.src, '*', [channel.port2]);
}
function onFrameMessage(e) {
const {id, result, error} = e.data;
const {resolve, reject, stack} = reqs[id];
delete reqs[id];
if (error) reject(Object.assign(new Error(error), {stack}));
else resolve(new Response(...result));
}
- iframe.html
<script src=iframe.js></script>
- iframe.js
let swPort, tabPort;
window.onmessage = e => {
if (e.data === location.href) {
tabPort = e.ports[0];
tabPort.onmessage = onTabMessage;
}
};
async function onTabMessage(e) {
const swr = await navigator.serviceWorker.ready;
const [port1, port2] = new MessageChannel();
port1.onmessage = onSWMessage;
swr.active.postMessage(e.data, [port2]);
// TODO: autocreate `transfer` from `e.data`
}
async function onSWMessage(e) {
tabPort.postMessage(e.data, [e.data.result?.[0]]);
}
- service worker
// sw
self.onmessage = async e => {
const {id, url, init} = e.data;
try {
const req = await fetch(url, init);
const buf = await req.arrayBuffer();
const {headers, statusText, status} = req;
const bufInit = {headers: Object.fromEntries(headers), status, statusText};
const result = [buf, bufInit];
e.ports[0].postMessage({id, result}, [buf]);
} catch (err) {
e.ports[0].postMessage({id, error: err.message});
}
};
- manifest.json
"web_accessible_resources": [{
"resources": ["iframe.html"],
"matches": ["<all_urls>"],
"use_dynamic_url": true
}]