Workaround Chrome iframe Service Worker Inheritance Limitation


As discussed on chromium.org in Sep 2018, Chrome iframe has a service worker (SW) inheritance limitation with srcdoc and about:blank. This limitation invalidates the offline and cache capabilities provided by service worker for progressive web app (PWA), if iframe with srcdoc or about:blank is used in PWA. This issue can hurts PWA, e.g., a PWA loads a large size font to browser top window and its srcdoc or about:blank iframes, because the font cached by top window service worker can’t be shared to the iframes. Each of the iframes needs to load the font individually. In addition to loss of SW cache capability, SW offline capability is also lost! It means the PWA doesn’t work offline even if it uses SWs😭

By my tests on Chrome 102, its iframes can inherit SW from parent only if its src has the same origin as parent. Thus, I propose to load iframe content from “the same origin host” to iframe src, instead of parent to iframe srcdoc or about:blank iframe. Why do I emphasize the same origin host with double quotes? Actually, I mean a virtual same origin host created by service worker! We know service worker can intercept HTTP requests for PWA. Thus, we can use this interception capability to create a virtual host, specially, a same origin one! Fortunately, we can use workbox-routing to easily create routes as a virtual host. The remaining bussiness is what’s the data flow? It is given below:

  1. The parent window generates HTML content for iframe and a randomly generated virtual pathname (In my implementation, I generate a string of 1 to 9 digits.)
  2. The parent window transfers the content and pathname to the SW. (In my implementation, I use postMessage. IndexedDB implementation is possible.)
  3. The SW receives the content and pathname. (Because I use postMessage implementation, SW needs to save them to a memory buffer for a later HTTP request.)
  4. The SW notifies the parent it’s ready to serve the virtual HTML file. (In my implementation, I use a MessageChannel connecting the parent and the SW. The SW uses it to notify.)
  5. The parent loads an iframe by assigning its src to the same origin of the parent + the virtual pathname (I call it “virtual same origin URL.”).
  6. The SW intercepts virtual same origin URLs from iframes, fetches HTML data from the memory buffer, and responses them to iframes. (In my implementation, I uses workbox-routing to create a route to intercept virtual same origin URL request.)

My implementation of this proposal works on Chrome Desktop, Android Chrome, and iOS Safari! This proposal solves the service worker inheritance issue of PWA with srcdoc or about:blank iframes to provide the cache and offline capabilities from service worker to those iframes. It’s also a cross main browsers solution!

Appendix – an example implementation of the proposal

  1. Parent tansfers data to SW:
const messageChannel = new MessageChannel();

// The following creates a virtual same origin HTML file with a random pathname, which avoiding multiple requests incorrectly use the same name.
// The virtual HTML file is sent to the service worker.
const pathname = "/_" + Math.floor(Math.random() * 1e9);

return new Promise<string>(ok => {
  // Wait VIRTUAL_HTML.
  messageChannel.port1.onmessage = (event) => {
    if (event.data.type === 'VIRTUAL_HTML' && event.data.pathname === pathname) {
      ok(pathname);
    }
  }

  // Send VIRTUAL_HTML.
  Globals.getServiceWorkerReg().then(serviceWorkerReg => {
    serviceWorkerReg.active?.postMessage({
      type: 'VIRTUAL_HTML',
      pathname,
      html,
    }, [messageChannel.port2]);
  });
});
  1. SW receives and buffers data, then notifies parent:
case 'VIRTUAL_HTML':
  // Transfer HTML contents for iframe from window.
  virtualHtmls.push({
    pathname: event.data.pathname,
    html: event.data.html,
  });
  event.ports[0].postMessage({ type: 'VIRTUAL_HTML', pathname: event.data.pathname })
  break;
  1. Parent loads iframe with virtual same origin URL:
}).then((pathname) => {
    this.iframe.src = pathname;
    this.element.appendChild(this.iframe);
});
  1. SW workbox-routing intercepts the virtual same origin URL and responses it:
registerRoute(({ url }) => {
  if (url.origin === self.location.origin && virtualHtmls.some((v) => v.pathname === url.pathname)) {
    return true;
  }
  return false;
}, async ({ url }) => {
  const dataIndex = virtualHtmls.findIndex(v => v.pathname === url?.pathname);
  const data = virtualHtmls.splice(dataIndex, 1)[0];
  const headers = new Headers({
    'Content-Type': 'text/html'
  });
  return new Response(data.html, {
    headers
  });
});

Leave a Reply