[翻訳] 2016

WBOY
WBOYオリジナル
2016-06-24 11:27:241161ブラウズ

元の記事は 2016 年 1 月 25 日に公開されました

さて、わかりました。1 月にこれが何なのか、今年のベストは何なのかを発表するのは少し大胆ですが、Web ストリーム API が示す可能性本当に興奮します。

TL;DR: ストリーミングは、クラウドをバットに変換したり、MPEG を GIF にエンコードしたりするなど、多くの興味深いことを行うために使用できますが、最も重要なのは、ストリーミングをサービス ワーカーと組み合わせてコンテンツを配信する最速の方法になることです。

ストリーミング?彼らは何が得意ですか

もちろんそれは何かです

Promise は非同期に取得された値を表す良い方法ですが、複数の値がある場合はどうなるでしょうか?あるいは、最大値から分割されて徐々に到達する部分的な値についてはどうでしょうか?

次の部分を含む画像を取得して表示したいとします:

    インターネットからデータを取得します
  1. それを実行し、圧縮データを元のピクセル データに変換します。
  2. レンダリングしてみます
それを段階的に実行することも、ストリーム処理を使用することもできます。

応答を少しずつ処理すると、画像の一部をより速くレンダリングできます。データの処理とフェッチが並行して行われるため、画像全体をより高速にレンダリングすることもできます。これがストリーム処理です!ネットワークからストリームを読み取り、圧縮データからピクセルデータに変換し、同時に画面に描画します。

イベントでも同様のことができますが、ストリームには次の利点があります:

  • 開始/終了認識 - ストリームは無限になる可能性がありますが
  • 未読の値のバッファリング - リスナー内でのデータ接続が成功する前に、イベントによって接続が失われます
  • パイプ経由で接続 - ストリームをパイプ経由で流し、非同期シーケンスを形成できます
  • 組み込みのエラー処理 - エラーはストリームに伝播します
  • キャンセルをサポート - キャンセルメッセージはパイプに戻されます
  • フロー制御 - 読み取り速度に反応できます
最後の利点は非常に重要です。ビデオをダウンロードして表示していると想像してください。 1 秒あたり 200 フレームをダウンロードしてデコードできるが、1 秒あたり 24 フレームしか表示できない場合、デコードされたフレームの大量のバックログが原因で、最終的にメモリが不足する可能性があります。

ここでフロー制御が必要になります。レンダリング ストリームは、デコードされたストリームからデコードされたフレームを 1 秒あたり 24 回プルします。したがって、デコーダは、デコード中のレートがフレームの読み取りレートよりもはるかに大きいことに気づき、デコード レートを下げます。次に、ネットワーク ストリームは、データを取得する速度がデコードされる速度よりもはるかに速いため、ダウンロード速度が遅くなることを認識します。

ストリームとリーダー間の密接な関係により、ストリームは 1 つのリーダーのみを使用できます。ただし、未読のストリームは「ティード」にすることができます。これは、同じデータを持つ 2 つのストリームに分割できることを意味します。この場合、T プロセッサは両方のリーダーのバッファリングされたデータを保持します。

さて、これらは単なる理論であり、2016 年の最高のトロフィーを渡す準備がまだできていないことはわかっていますが、ぜひ一緒に読んでください。

ブラウザはデフォルトでストリームを使用します。ブラウザがページ、画像、ビデオをダウンロードし、同時に表示しているのは、ストリーム処理のおかげです。ただし、標準化の取り組みのおかげでストリームがスクリプトに公開されるようになったのは最近のことです。

ストリームとフェッチ API

フェッチ仕様で定義されている応答オブジェクトでは、さまざまな形式で応答を読み取ることができますが、response.body を使用すると、基になるストリームに直接アクセスできます。 response.body は、現在の Chrome 安定版ブラウザのバージョンですでにサポートされています。

ヘッダーに依存したり、応答全体をメモリに保持したりせずに、応答のコンテンツの長さを取得したいとします。これはストリーミング経由で実行できます:

// fetch() 返回一个promise一旦头部被获取就resolvefetch(url).then(response => {  // response.body是一个可读流  // 调用getReadeer()函数让我们可以独占到流内容的访问  var reader = response.body.getReader();  var bytesReceived = 0;  // read() 返回一个promise当值被读取到时会进行resolve  reader.read().then(function processResult(result) {    // 结果对象含有一下两个属性    // done  - 当流给予了你所有数据的时候会设为true    // value - 一些数据。当done是true的时候,为undefined    if (result.done) {      console.log("Fetch complete");      return;    }    // fetch流的result.value是一个Uint8Array    bytesReceived += result.value.length;    console.log('Received', bytesReceived, 'bytes of data so far');    // 阅读更多,再次调用这个函数    return reader.read().then(processResult);  });});

例を見てください (1.3MB)

この例では、サイズが 1.3MB、解凍後のサイズが 7.7MB の gzip 圧縮された圧縮 HTML ファイルをサーバーから取得します。ただし、結果はキャッシュに保存されません。各ブロックのサイズは記録されますが、各ブロック自体はガベージ コレクションされます。

result.value は任意のストリームによって提供されます。文字列、数値、日付オブジェクト、画像データ、DOM 要素など、何でもかまいません。ただし、フェッチ ストリームでは、常にバイナリの Uint8Array です。応答全体は、各 Unit8Array がグループ化されたものです。応答をテキスト形式にしたい場合は、TextDecoder を使用できます:

var decoder = new TextDecoder();var reader = response.body.getReader();// read() returns a promise that resolves// when a value has been receivedreader.read().then(function processResult(result) {  if (result.done) return;  console.log(    decoder.decode(result.value, {stream: true})  );  // Read some more, and recall this function  return reader.read().then(processResult);});

{stream:true} は、UTF-8 コードポイントを渡すときに result.value が壊れた場合に、デコーダがバッファを保持することを意味します。 , 1 文字 ♥ は [0xE2, 0x99, 0xA5] の 3 ビットで表されます。

TextDecoder 目前是有一些笨拙,但是他有可能在未来变为一个变换流(一旦变换流被定义)。变换流是一个拥有可写流 .writable 和可读流 .readable 的对象。它通过可写流获取块文件并且处理他们,然后通过可读流传出内容。使用变换流会是这样子的:

假设这是未来的代码:

var reader = response.body  .pipeThrough(new TextDecoder()).getReader();reader.read().then(result => {  // result.value will be a string});

浏览器应该有能力优化到上述程度,因为响应流和 TextDecoder 变换流都是浏览器本身的。

取消fetch

我们可以利用 stream.cancel() 或者 reader.cancel() 来取消一个流(同样的对应fetch使用 response.body.cancel() )。fetch会对此作出停止下载的反应。

观看示例 (注意一下JSBin给予我的神器的随机URL).

这个示例是搜索一个大型文档里面的一个术语,每次只在内存中保留一小部分,一旦找到匹配的部分就停止获取。

无论如何,这些都是2015的东西了。下面将会是一些新的东西。

创建你的可读取流

在Chrome Canary版本中使用"实验网络平台功能",你可以创建属于你自己的流。

var stream = new ReadableStream({  start(controller) {},  pull(controller) {},  cancel(reason) {}}, queuingStrategy);
  • start 会被马上调用。使用它去设置一些底层数据源(这意味着,你可以从任何地方去得到你的数据,数据可能是事件、其他流、一个变量、一个字符串)。如果你从这里返回一个promise对象并且reject,它会通过流发送一个错误。
  • pull 会在你的流缓冲尚未满的时候被调用,他会被重复调用直至你的缓冲区已经满了。同样地,如果你从这里返回一个promise并且他被reject了,它会通过流发送一个错误。另外,在返回的promise完成前, pull 不会被再次调用。
  • cancel 会在stream被取消的时候调用。可以利用它去取消任意的底层数据。
  • queuingStrategy 定义了这个流理想情况下可以缓冲多少数据,默认是一个 - 我不打算深入这一部分, 规范上面有更多细节

至于说到 controller :

  • controller.enqueue(whatevet) - 让数据在流的缓冲区中排队
  • controller.close() - 标志了流的结束
  • conttoller.error(e) - 标志了一个终端错误
  • controller.desiredSize - 缓冲区中留有的数据数量,当缓冲区溢出的时候可能是负数。这个数据是利用 queuingStrategy 计算出的。

所以如果我想创造一个流可以每秒产生一个随机数,直到他产生的数字大于0.9.我会这么做:

var interval;var stream = new ReadableStream({  start(controller) {    interval = setInterval(() => {      var num = Math.random();      // Add the number to the stream      controller.enqueue(num);      if (num > 0.9) {        // Signal the end of the stream        controller.close();        clearInterval(interval);      }    }, 1000);  },  cancel() {    // This is called if the reader cancels,    //so we should stop generating numbers    clearInterval(interval);  }});

观看运行示例 . 注意: 你需要使用Chrome Canary版本并且启用 chrome://flags/#enable-experimental-web-platform-features 。

你可以控制什么时候把数据传送给 controller.enqueue 。你可以在你拥有数据需要发送的时候调用,使你的流成为一个“推送源”。当然你也可以选择等待到 pull 被调用,然后使用它作为一个信号去收集底层数据然后让他 enqueue ,使你的流成为“拉取源”。或者你可以结合两种方式,无论你想用哪种。

遵循 controller.desiredSize 意味着流正在用最有效率的方式传输数据。这被称作“背压支持”(backpressure support),意味着你的流会对读取器的读取速率作出反应(和之前提到的视频解码例子一样)。然而,忽略掉 desiredSize 并不会破坏什么东西,除了你可能会消耗掉整个设备的内存。在规范上面有 创造一个背压支持的流 的例子。

创建一个自己的流并不是一件十分有趣的是,因为他们是新的,所以没有特别多的API支持他们,不过有这么一条

new Response(readableStream)

你可以创建一个HTTP响应对象,他的body是一个流,然后你可以把这个对象应用到service worker上。

缓慢地提供一个字符串

观看示例 . 注意: 你需要使用Chrome Canary版本并且启用 chrome://flags/#enable-experimental-web-platform-features 。

你会看到一个被(故意)渲染的非常慢的HTML页面。这个响应完全由service worker生成。下面是代码:

// In the service worker:self.addEventListener('fetch', event => {  var html = '…html to serve…';  var stream = new ReadableStream({    start(controller) {      var encoder = new TextEncoder();      // 我们目前的位置是在HTML中      var pos = 0;      // 每一次服务器推送的个数      var chunkSize = 1;      function push() {        // 推送完毕了吗        if (pos >= html.length) {          controller.close();          return;        }        // 推送一些html。并且把它转化为一个utf-8数据的Unit8Array        controller.enqueue(          encoder.encode(html.slice(pos, pos + chunkSize))        );        // 移动位置        pos += chunkSize;        // 5毫秒后再次推送        setTimeout(push, 5);      }      // 出发      push();    }  });  return new Response(stream, {    headers: {'Content-Type': 'text/html'}  });});

当浏览器读取到相应的内容他希望读取到 Unit8Array 块,如果传入了其他格式的数据比如一个空白的字符串,他会读取失败。幸运的是, TextEncoder 可以传入一个字符串,然后返回一个 Unit8Array 格式的比特代表这是字符串。

诸如 TextDecoder , TextEncoder 在以后会成为一个变换流。

提供一个变换流

像我说的那样,变换流尚未被定义,但是你可以通过从其他数据源创造一个可读流来实现相同目的。

"cloud" から "butt"

例を見てください 注: Chrome Canary バージョンを使用し、 chrome://flags/#enable-experimental-web-platform-features を有効にする必要があります。

このページは Wikipedia のクラウド コンピューティング関連記事から抜粋したものですが、その中の「クラウド」という単語はすべて「尻」に置き換えられています。この利点は、ソースからデータをダウンロードしながらコンテンツを変換できることです。

以下に、エッジの例を含むコードを示します。

MPEG to GIF

ビデオコードは非常に効率的ですが、携帯電話では自動的に再生できません。 GIF 形式は携帯電話でも自動的に再生できますが、サイズが非常に大きくなります。さて、これは非常に愚かな解決策です:

例を見てください 注: Chrome Canary バージョンを使用し、 chrome://flags/#enable-experimental-web-platform-features を有効にする必要があります。

ストリーミングは、MPEG フレームがまだデコードされている間に GIF の最初のフレームを再生できるため、ここでは便利です。

だから、やってみよう! 26 MB GIF の送信に必要なのは、0.9 MB MPEG だけです。完璧!ただし、リアルタイムではないため、CPU リソースを大量に消費します。ブラウザでは、携帯電話でのビデオ、特にミュートされたビデオの自動再生を許可する必要があります。 Chromes はこれに取り組んでいます。

開示: 実際、MPEG 全体をダウンロードした後でのみ再生を開始したため、デモでは少し騙されたように感じました。ストリームを使用して取得できればいいのですが、OutOfSkillError が発生しました。同様に、GIF はダウンロード時にループ再生されるべきではありません。これは研究が必要なバグです。

マルチソース ストリームを作成してページのレンダリング時間を短縮する

これは、おそらくストリーム + Service Worker の最も実用的なアプリケーションです。パフォーマンスだけを見ても、その利点は非常に大きいです。

数か月前、私はウィキペディアのオフラインファーストバージョンのサンプルを作成しました。私は、高速で、進歩的なネットワーキング戦略に従い、より最新の機能で強化された Web アプリケーションを作成したいと考えていました。

以下に引用するパフォーマンス数値は、貧弱な 3G ネットワークをシミュレートした OSX ベースのネットワークに基づいています。

Service Worker が欠落しており、表示されるコンテンツはサーバーからのものです。パフォーマンスの最適化に多大な労力を費やしましたが、結果は次のようになります:

例を見てください

それほど悪くありません。パフォーマンスをさらに最適化するために、オフラインファーストの利点を導入するために Service Worker を追加しました。結果毛織物?

例を見てください

ご覧のとおり、スクロールせずに見える範囲のレンダリングは高速化されていますが、コンテンツのレンダリングにはまだ大きな改善の余地があります。

最も速いレンダリング方法は、ページ全体をキャッシュからロードすることですが、これはすべての Wikipedia をキャッシュする必要があることを意味します。逆に、CSS、JavaScript、ヘッダーを含むページを提供することで、非常に高速な最初の画面のレンダリング エクスペリエンスを実現し、ページの JavaScript を使用して記事のコンテンツを取得します。ここでパフォーマンスが低下します - クライアント側のレンダリングです。

HTML は、サーバーまたは Service Worker からダウンロードされるとレンダリングされます。ただし、JavaScript を使用してページのコンテンツを取得し、ストリーム インタープリターの代わりに innerHTML を使用してレンダリングします。このため、このコンテンツ部分はダウンロードが完了するまでレンダリングされず、これが 2 秒の遅延の原因です。ダウンロードするコンテンツが多ければ多いほど、ストリーミング不足の影響は大きくなります。残念ながら、Wikipedia の記事は非常に大きくなります (Google の記事のサイズは 100k)。

これが、私が JavaScript を利用した Web アプリやフレームワークについて不満を言っているのを目にする理由です。これらは常にストリームを破棄し、最初から開始するため、パフォーマンスが大幅に低下します。

パフォーマンスを一部改善するために、プリフェッチ ストリームと疑似ストリームを使用しようとしました。疑似ストリーミングはかなりハックな方法です。ページは記事のコンテンツを取得し、ストリームを使用してそれを読み取り、読み取ったデータ量が 9k に達すると、innerHTML メソッドを使用してコンテンツを書き込み、残りのコンテンツを取得したら、再度 innerHTML を使用して書き込みます。いくつかの要素を 2 回作成するため、これは実際には非常に恐ろしいことですが、それだけの価値はあります。

例を見てください

このように、ハッキングにより MI は改善されましたが、サーバー側のレンダリングと比較するとまだ遅れがあり、これは本当に容認できません。さらに、innerHTML を介してページにコンテンツを追加すると、通常のコンテンツ解析とは動作が異なります。インラインの 3f1c4e4b6b16bbbd69b2ee476dc4f83a は実行されないことに注意してください。

これは流れの時間です。空のシェルを提供して JS に埋めさせるだけのことはあきらめました。 Service Worker にストリームを構築するように依頼しました。このストリームのヘッダー部分はキャッシュから取得され、本体部分はネットワークから取得されます。彼のアプローチは、Service Worker でレンダリングすることを除いて、サーバー レンダリングと一致しています。ここでストリームが介入します。空のシェルを提供して JS に設定させる代わりに、ヘッダーはキャッシュから取得され、本体はネットワークから取得されるストリームをサービス ワーカーに構築させます。これはサーバー レンダリングに似ています。 Service Worker 内:

观看示例 . 注意: 你需要使用Chrome Canary版本并且启用 chrome://flags/#enable-experimental-web-platform-features 。

利用servce worker+流的方式,你可以得到一个几乎是瞬时的首屏体验,然后通过管道读取一小部分来自网络的内容从而成功击败了常规的服务器渲染。内容是通过常规HTML渲染的,所以你得到的是流,这样子你和正常的人工DOM添加没有任何差别。

渲染时间对比 。

穿越流

因为piping尚未被支持,因此必须手动去合成流,这让事情有一些混乱:

var stream = new ReadableStream({  start(controller) {       // 得到每一个页面部分的响应promise    // 开始与结束来自于缓存    var startFetch = caches.match('/page-start.inc');    var endFetch = caches.match('/page-end.inc');    // 中间部分来自于网络,使用了一个回调的方法    var middleFetch = fetch('/page-middle.inc')      .catch(() => caches.match('/page-offline-middle.inc'));    function pushStream(stream) {      // 在流上加锁      var reader = stream.getReader();      return reader.read().then(function process(result) {        if (result.done) return;        // 把值推向合成流        controller.enqueue(result.value);        // 继续读取处理        return read().then(process);      });    }    // 得到响应的开始部分    startFetch      // 把处理后的部分推向合成流      .then(response => pushStream(response.body))      // 得到中间的响应部分      .then(() => middleFetch)      // 把处理后的内容推向合成流      .then(response => pushStream(response.body))      // 得到相应的结尾部分      .then(() => endFetch)      // 把处理后的内容推向合成流      .then(response => pushStream(response.body))      // 结束流,我们完成了。      .then(() => controller.close());  }});

有一些模板语言,例如 Dust.js ,是利用流输出他们的内容的,并且在模板中把流当成值进行处理,用流的方式传输内容甚至会进行HTML转移。唯一缺失的就是支持网络流。

流的未来

除了可读流,流规范仍然处于开发中。但是你现在能做的已经非常不可思议了。如果你仍然希望改进内容站点的性能并且提供一个更好的离线优先体验而又不愿意进行重构。构建一个使用流的service worker是最简单的办法。这就是我打算对这个博客所做的。

在网络上拥有一个原始流意味着我们可以通过所有浏览器兼容的方式获取脚本。包括:

  • Gzip/deflate
  • 音频/视频代码
  • 图像代码
  • 流媒体HTML/ XML解析器

虽然现在仍然很早,但是如果你想把你的API转为流,你可以 参考 在某种情况下进行一些polyfill。

流媒体是浏览器最大的资产,而2016年是把他向JavaScript解锁的一年。

感谢 Dominic Szablewski的JS MPG1解码器 ( 看看他说了些什么 ),和 Eugene Ware的GIF编码器 。还得感谢Domenic Denicola, Takeshi Yoshino, Yutaka Hirano, Lyza Danger Gardner, Nicolás Bevacqua, 和 Anne van Kesteren的指正和想法。是的,这需要这么多人来找我的错误。

扫码关注w3ctech微信公众号

声明:
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。