2016,屬于web stream的一年

譯者的話(也就是mandy的話):
stream不是一個(gè)新的概念,瀏覽器一直都在使用stream,但是并沒有暴露給前端開發(fā)者,我們也因此錯(cuò)過了一個(gè)巨大的寶藏。好在一些新的標(biāo)準(zhǔn)和瀏覽器實(shí)現(xiàn)中,我們可以染指stream了。
譯文夾帶著個(gè)人理解,如果想深入研究請(qǐng)聯(lián)系譯者或移步原文the year of web streams

好吧,在一月份就斷言今年是某個(gè)東西的一年有點(diǎn)草率,但是web stream的潛能真的讓我很興奮。

streams可以用來做一些有趣的事情,比如,“把云換成屁股”(一個(gè)惡趣味又無語的項(xiàng)目https://github.com/panicsteve/cloud-to-butt), 又比如把MPEG轉(zhuǎn)碼成GIF。但是最重要的,streams結(jié)合service workers,可以使前端渲染性能,提高一個(gè)level。

streams適合用來做什么?

假設(shè)我們要拉取和顯示一張圖片,步驟是這樣的

  1. 通過網(wǎng)絡(luò)拉取圖片
  2. 解析圖片,把圖片數(shù)據(jù)處理成可以渲染的像素?cái)?shù)據(jù)。
  3. 渲染圖片

我們可以一步一步來做這些事情,也可以用stream來做。

如果我們可以一個(gè)bit一個(gè)bit地處理response,圖片局部可以渲染得更快,甚至可以整張圖片都可以渲染得更快,因?yàn)閳D片還在請(qǐng)求中的時(shí)候就可以被并行地解析。這就是stream。

我們用事件也可以做類似的事情,但是用stream的好處是

  1. start和end可被感知(雖然stream可能是無限長(zhǎng)的)
  2. 緩存還未被讀取的數(shù)據(jù)(用event的話事件發(fā)生之前的數(shù)據(jù)我們讀不到,stream是源源不斷的,event是節(jié)點(diǎn)型的)
  3. stream鏈(通過pipe來實(shí)現(xiàn)stream序列)
  4. 內(nèi)置的錯(cuò)誤處理機(jī)制(錯(cuò)誤可以在pipe中傳遞)
  5. 可取消的,并且被取消的數(shù)據(jù)可以備份
  6. 流控制,我們可以根據(jù)播放速度來控制數(shù)據(jù)接收速度。

最后一點(diǎn)很重要,想象一下,我們用stream來接收和播放一個(gè)視屏,如果我們一秒鐘能下載和解析200幀的視頻, 而只想一秒鐘播放24幀,那么我們很容易就積壓太多的幀數(shù)而耗盡內(nèi)存。

所以有了流控制的概念,渲染stream從解碼stream中一秒鐘拉取24幀,解碼stream發(fā)現(xiàn)自己解碼的速度比渲染快,就會(huì)慢下來。然后請(qǐng)求stream發(fā)現(xiàn)自己請(qǐng)求數(shù)據(jù)的速度比解碼stream快,也會(huì)跟著慢下來。

因?yàn)閟tream和播放器的強(qiáng)關(guān)聯(lián),一個(gè)stream只能對(duì)接一個(gè)播放器,不過,未被解析的stream可以有旁路,這種情況下,這個(gè)被分叉的stream可以充當(dāng)緩存數(shù)據(jù)的角色。

瀏覽器本身就是用streams來接收數(shù)據(jù)的,我們看到瀏覽器上的頁面/圖片/視頻可以一點(diǎn)一點(diǎn)逐步就顯示,都是streams的功勞。這足以體現(xiàn)streams的優(yōu)異之處,然而直到最近,在standardisation effort的貢獻(xiàn)下,我們終于可以用js操作這些streams。

Streams + the fetch API

fetch spec中定義了response 對(duì)象,可以暴露各種各樣的格式的屬性,responese.body就是其中一個(gè),我們就是通過這個(gè)屬性來接觸底層的streams。最新版本的chrome已經(jīng)支持responese.body

假如我們想拿到response的contrnt-length,即使不通過header,也不用把整個(gè)response緩存下來,用streams可以這樣做

// fetch() returns a promise that
// resolves once headers have been received
fetch(url).then(response => {
    // response.body is a readable stream.
    // Calling getReader() gives us exclusive access to
    // the stream's content
    var reader = response.body.getReader();
    var bytesReceived = 0;

    // read() returns a promise that resolves
    // when a value has been received
    reader.read().then(function processResult(result) {
        // Result objects contain two properties:
        // done  - true if the stream has already given
        //         you all its data.
        // value - some data. Always undefined when
        //         done is true.
        if (result.done) {
           console.log("Fetch complete");
           return;
        }

        // result.value for fetch streams is a Uint8Array
        bytesReceived += result.value.length;
        console.log('Received', bytesReceived, 'bytes of data so far');

        // Read some more, and call this function again
        return reader.read().then(processResult);
    });
});

demo演示 (1.3mb)

demo里面fetch了1.3mb的gzi過的html,解壓以后7.7mb,但是這么龐大的體積不會(huì)存在內(nèi)存中,我們只記錄內(nèi)容的大小,內(nèi)容本身會(huì)被gc掉,這就是streams的優(yōu)勢(shì)。

result.velue可能是stream創(chuàng)建者創(chuàng)建的任何格式,在上面的例子中是一個(gè)二進(jìn)制數(shù)據(jù),如果你想轉(zhuǎn)成text格式,可以用 TextDecoder

var decoder = new TextDecoder();
var reader = response.body.getReader();

// read() returns a promise that resolves
// when a value has been received
reader.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字符被截?cái)嗟臅r(shí)候,decoder會(huì)緩存上一個(gè)數(shù)據(jù),知道整個(gè)被截?cái)嗟淖址梢员唤馕龀鰜?。比如?? 這樣的字符是三字節(jié)的,可能發(fā)生截?cái)唷?/p>

TextDecoder現(xiàn)在看來有些笨拙,但它最有希望成為將來的transform stream(只要瀏覽器端有這個(gè)定義)。一個(gè)transform stream同時(shí)擁有writeable stream和readable stream,它用writeable stream來處理數(shù)據(jù),然后下一個(gè)stream可以讀它的readable stream。用transform stream來實(shí)現(xiàn)上面的例子:

var reader = response.body
  .pipeThrough(new TextDecoder()).getReader();

reader.read().then(result => {
  // result.value will be a string
});

這是瀏覽器以后應(yīng)該做的優(yōu)化,畢竟response stream和TextDecoder transform stream 都是瀏覽器提供的。

取消一個(gè)fetch

stream.cancel()或者response.body.cancel()就可以取消一個(gè)fetch行為,fetch就會(huì)響應(yīng)的取消download。

View demo
這個(gè)demo搜索一個(gè)大的html文件,一旦找到了匹配的內(nèi)容,整個(gè)請(qǐng)求就取消了,幾乎不用什么內(nèi)存。

看到這里是不是覺得有點(diǎn)意思了,不過,這些都是2015年的東西了,2016,不只這些

創(chuàng)建自己的readable stream

如果開啟chrome的Experimental web platform features,你可以創(chuàng)建自己的readable stream。

var stream = new ReadableStream({
  start(controller) {},
  pull(controller) {},
  cancel(reason) {}
}, queuingStrategy);

舉個(gè)例子,比如我們要持續(xù)輸出一個(gè)random一個(gè)數(shù)字,到隨機(jī)數(shù)大于0.9時(shí)結(jié)束

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);
  }
});

demo演示. Note: 需要把chrome的chrome://flags/#enable-experimental-web-platform-features打開

你可以自己決定在什么時(shí)候傳值給controller.enqueue,當(dāng)你有數(shù)據(jù)要傳的時(shí)候再調(diào)用就可以了。

慢速吐出一個(gè)string

先上效果.Note: 需要把chrome的chrome://flags/#enable-experimental-web-platform-features打開

你可以看到一個(gè)緩慢渲染HTML的頁面(故意的),這個(gè)response整個(gè)都是在service worker中生成的。

// In the service worker:
self.addEventListener('fetch', event => {
  var html = '…h(huán)tml to serve…';

  var stream = new ReadableStream({
    start(controller) {
      var encoder = new TextEncoder();
      // Our current position in `html`
      var pos = 0;
      // How much to serve on each push
      var chunkSize = 1;

      function push() {
        // Are we done?
        if (pos >= html.length) {
          controller.close();
          return;
        }

        // Push some of the html,
        // converting it into an Uint8Array of utf-8 data
        controller.enqueue(
          encoder.encode(html.slice(pos, pos + chunkSize))
        );

        // Advance the position
        pos += chunkSize;
        // push again in ~5ms
        setTimeout(push, 5);
      }

      // Let's go!
      push();
    }
  });

  return new Response(stream, {
    headers: {'Content-Type': 'text/html'}
  });
});

用一個(gè)stream接收多種資源,減少頁面渲染時(shí)間

這恐怕是最實(shí)用的運(yùn)用場(chǎng)景了。對(duì)前端性能有極大的提升。
幾個(gè)月前我寫了一個(gè)首屏離線化的demo 我當(dāng)時(shí)的目標(biāo)是寫一個(gè)出色的web app,它必須有很快的響應(yīng)速度,于是漸進(jìn)增強(qiáng)地用了service worker的一些特性。

給出一些當(dāng)時(shí)的數(shù)據(jù),osx的模擬器下,3g網(wǎng)絡(luò):

  1. 服務(wù)端渲染

服務(wù)端渲染

數(shù)據(jù)還不錯(cuò),然后我加入了一些service worker的特性來進(jìn)一步優(yōu)化:


前端渲染

首屏渲染時(shí)間減少了很多,但是首屏之后的渲染反而退步了。

最快的方法當(dāng)然緩存整個(gè)頁面,但是這樣子太浪費(fèi)內(nèi)存了。所以我緩存了頭部,css和js,這樣可以使首屏速度最快。然后拉取內(nèi)容,用js來渲染。這也是大部分web應(yīng)用的做法: 客戶端渲染。

無論用直接的網(wǎng)絡(luò)請(qǐng)求還是用service worker,HTML都是邊下載邊渲染的,但是我用js來請(qǐng)求內(nèi)容,再用innerHTML插入內(nèi)容,整個(gè)頁面是等數(shù)據(jù)回來才開始渲染的,這也是上圖相比服務(wù)端渲染慢2秒的原因。
內(nèi)容越大,比服務(wù)端渲染的速度就越慢。

這就是我抱怨服務(wù)端渲染的web app或者框架的原因,它們從一開始就拋棄了 stream,造成了這么差的性能。

于是我想通過偽stream來挽回一些性能,做法十分hacky,頁面用fetch來拉取內(nèi)容,并且用stream來讀取內(nèi)容,每次獨(dú)到的內(nèi)容有9k,便用innerHTML渲染出來,這樣來模擬瀏覽器逐漸渲染html的特性。


偽stream

可以看到,性能提升了不少,但是還是比不上服務(wù)端渲染,而且,用innerHTML來插入標(biāo)簽的方法跟原生渲染標(biāo)簽還是有區(qū)別的,最明顯的,innerHTML的script標(biāo)簽里的代碼不被執(zhí)行.

這時(shí)候真正的stream來了,不同于提供一個(gè)空殼,然后用js來填充內(nèi)容,我讓service worker構(gòu)建一個(gè)stream,這個(gè)stream會(huì)把渲染好的頁面吐給瀏覽器(頭部依舊是來自緩存,內(nèi)容依舊從網(wǎng)上下載),這就像是服務(wù)端渲染,不過是在service worker里進(jìn)行的。


真stream

用stream加service worker意味著你可以瞬間渲染首屏內(nèi)容,然后用小于或等于服務(wù)端渲染的時(shí)間(請(qǐng)求的內(nèi)容比服務(wù)端渲染還少)來渲染余下的內(nèi)容。內(nèi)容渲染還是用的原生的瀏覽器的HTML parser,不會(huì)像innerHTML那樣丟失一些特性。

附上對(duì)比視頻

除了readable stream,其他stream還在開發(fā)中,但是我們可以做的事情已經(jīng)很不可思議了,如果你想提高一個(gè)重內(nèi)容型的web應(yīng)用而且想實(shí)現(xiàn)首屏離線化,又不想重構(gòu)你的代碼,那么用stream加service worker是簡(jiǎn)單的辦法。

在web中有一個(gè)原生stream支持意味著我們可以染指瀏覽器各種stream相關(guān)的能力,比如

  1. Gzip/壓縮
  2. 音視頻解碼
  3. 圖片解碼
  4. HTML/XML parser
    現(xiàn)在說這些還有點(diǎn)早,但是如果你想在stream上面開發(fā)一些自己的API,reference implementation這是一些pollyfill。

流是瀏覽器很有價(jià)值一個(gè)特性,并且在2016年會(huì)對(duì)javascript解鎖!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容