把iframe当作worker使用

1,444 阅读1分钟

will-a-browser-give-an-iframe-a-separate-thread-for-javascript发现一个有趣东西

图片.png

Chrome对于不同domain的域名是使用不同的UI线程,也就是iframe不会阻塞宿主,那么意味着iframe也可以当作worker使用了

那么使用zstd decoder简单验证和对比web worker。

宿主网页HTML, host在 127.0.0.1:8080

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe worker</title>
  </head>
  <body>
    <div id="count"></div>
    <script>
      let i = 0;
      let div = document.getElementById('count');
      setInterval(() => {
        div.innerText = i++;
      }, 100);
      // 测试通讯数据传递速度是多少
    </script>

    <!-- <iframe src="http://192.168.1.3:8081/index.html" frameborder="0"></iframe> -->
    <iframe src="http://127.0.0.1:8081/index.html" frameborder="0"></iframe>
  </body>
</html>

iframe host在127.0.0.1:8081

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe worker</title>
  </head>
  <body>
    <button id="spin">spin</button>
    <script>
      const spin = document.getElementById('spin');
      spin.addEventListener('click', () => {
        const start = Date.now();
        while (Date.now() - start < 1000) {}
      });
    </script>
  </body>
</html>

在chrome 91上点击spin按钮宿主网页会卡顿1s,怎么和文中的描述不一致呢?同源策略不是protocol+域名+port相同才算同源么?这里一个端口8080,另一个是8081,难道是127.0.0.1特殊ip导致的?所以分别测试一下组合

宿主:8080iframe:8081
127.0.0.1127.0.0.1阻塞
127.0.0.1192.168.1.3不阻塞
192.168.1.3127.0.0.1不阻塞
192.168.1.3192.168.1.3阻塞

这就奇怪了,难道 Chrome 在这里的同源策略检测有Bug? 熟悉C++大佬们可以帮忙翻翻Chromium源码 html_iframe_element.cc帮忙查找原因

既然确定了有OOPIF的策略,那么说明是iframe当作worker是可行的,接下来是实现zstd decode in iframe worker

宿主依然在8080端口,但是iframe使用ip是192.168.1.3

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe worker</title>
  </head>
  <body>
    <div id="count"></div>
    <script>
      let i = 0;
      let div = document.getElementById('count');
      setInterval(() => {
        div.innerText = i++;
      }, 100);
    </script>

    <iframe src="http://192.168.1.3:8081/index.html" frameborder="0"></iframe>
    <!-- <iframe src="http://127.0.0.1:8081/index.html" frameborder="0"></iframe> -->

    <script type="module">
      import { ZSTDDecoderWorker } from './ZSTDDecoderWorker.js';
      /** @type HTMLIFrameElement */
      const $worker = document.getElementsByTagName('iframe')[0];
      const channel = new MessageChannel();
      const decoder = new ZSTDDecoderWorker(1);

      Promise.all([
        fetch('./group1-shard1of2.bin.zst').then(i => i.arrayBuffer()),
        new Promise(resolve => ($worker.onload = resolve)),
        decoder.init(),
      ]).then(([zstBuffer]) => {
        console.log(zstBuffer);
        const data = zstBuffer.slice(0);

        $worker.contentWindow.postMessage({ time: Date.now() }, '*', [
          channel.port2,
        ]);

        const dataSendTime = Date.now();
        channel.port1.postMessage({ data, time: dataSendTime }, [data]);
        channel.port1.onmessage = async e => {
          const { data, time } = e.data;
          const dataReceivedTime = Date.now();
          console.log('data from msg channel', dataReceivedTime - time);
          console.log('iframe worker decode time', dataReceivedTime - dataSendTime);

          const dataInput = zstBuffer.slice(0);
          const decodeStartTime = Date.now();
          const output = await decoder.decode(new Uint8Array(dataInput));
          const decodeEndTime = Date.now();
          console.log('worker decode time', decodeEndTime - decodeStartTime);
        };
        // 经测试无法直接把buffer传递到iframe,需要使用MessageChannel的postMessage
        // $worker.contentWindow.postMessage(
        //   { zstBuffer, time: Date.now() },
        //   '*',
        //   [zstBuffer],
        // );
      });
    </script>
  </body>
</html>

iframe依然在8081端口

<!DOCTYPE html>
<html lang="zh">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>iframe worker</title>
  </head>
  <body>
    <button id="spin">spin</button>
    <script>
      const spin = document.getElementById('spin');
      spin.addEventListener('click', () => {
        const start = Date.now();
        while (Date.now() - start < 1000) {}
      });
    </script>

    <script type="module">
      import { ZSTDDecoder } from './zstddec.module.js';

      const zstdDecoder = new ZSTDDecoder();
      zstdDecoder.init();

      let msgChannelPort2;

      window.addEventListener('message', e => {
        console.log('msg from main frame', Date.now() - e.data.time);

        msgChannelPort2 = e.ports[0];
        msgChannelPort2.onmessage = async e => {
          const { data, time } = e.data;
          const msgReceivedTime = Date.now();
          console.log('msg from msg channel', msgReceivedTime - time);

          await zstdDecoder.init();
          const timeDecoderInit = Date.now();
          console.log('zstd init time', timeDecoderInit - msgReceivedTime);

          const output = zstdDecoder.decode(new Uint8Array(data));
          const decodeEndTime = Date.now();
          console.log('zstd decode cost', decodeEndTime - msgReceivedTime);

          msgChannelPort2.postMessage({ output, time: decodeEndTime }, [
            output.buffer,
          ]);
        };
      });
    </script>
  </body>
</html>

性能结果如下图,iframe worker在OOPIF下传递消息耗时比web worker下大的多

图片.png

一下是同源的iframe测试结果,同源iframe在同一个线程,msg channel是不耗时的

图片.png

可clone github仓库 iframe-worker-demo,自行探索下

图片.png