Why is it faster to fetch a resource, create an object URL, and set an iframe’s src attribute to that, than to set src to that resource directly?

Recently I was creating a print button for a report. I wanted the print button to use the PDF version of the report for cross-browser consistency. At first I fetch()ed the PDF, got the blob, created an object URL, and set the iframes src property to that URL.

Later I realized I could just set the iframe`s src to the PDF URL in the first place. But that turned out to be slower by ~400ms on a 5s request. In fact the speed difference seems to scale with the length of the request.

Even when just loading a JSON file, as in the snippet below, the speed difference persists.

Here’s a codepen where you can fiddle with the tests yourself. I tried re-arranging and adding cache-busting query-string parameter to eliminate. But fetch() is still faster ~90% of the time.

const iframe = document.getElementById("iframe");
const testUri = "https://hacker-news.firebaseio.com/v0/topstories.json";
const fastestEl = document.getElementById('fastest')

async function runTests() {
  const srcTime = await measureLoadTimeUsingSrc()
  const fetchTime = await measureLoadTimeUsingFetch()

  document.getElementById("srcTime").textContent = srcTime;
  document.getElementById("fetchTime").textContent = fetchTime;
  
  if (srcTime < fetchTime) {
    fastestEl.textContent = 'src'
  } else if (fetchTime < srcTime) {
    fastestEl.textContent = 'fetch'
  } else {
    fastestEl.textContent = 'tie'
  }
}

function measureLoadTimeUsingSrc() {
  return new Promise((resolve, reject) => {
    const startTime = performance.now()
    iframe.src = testUri;
    iframe.addEventListener(
      "load", 
      () => resolve(performance.now() - startTime), 
      { once: true }
    );
  });
}

function measureLoadTimeUsingFetch() {
  return new Promise((resolve, reject) => {
    const startTime = performance.now()
    fetch(testUri)
      .then((result) => result.blob())
      .then((blob) => {
        iframe.src = URL.createObjectURL(blob);
        iframe.addEventListener(
          "load", 
          () => resolve(performance.now() - startTime), 
          { once: true }
        );
      })
      .finally(() => URL.revokeObjectURL(iframe.src));
  });
}
<dl>
  <dt>Time using <code>iframe.src = '…'</code><dt>
  <dd id="srcTime">…</dd>
  <dt>Time using <code>fetch('…')</code><dt>
  <dd id="fetchTime">…</dd>
  <dt>Fastest</dt>
  <dd id="fastest">…</dd>
</dl>

<iframe id="iframe" hidden></iframe>
  
<button type="button" onclick="runTests()">
  Run tests
</button>