譯者的話(也就是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è)我們要拉取和顯示一張圖片,步驟是這樣的
- 通過網(wǎng)絡(luò)拉取圖片
- 解析圖片,把圖片數(shù)據(jù)處理成可以渲染的像素?cái)?shù)據(jù)。
- 渲染圖片
我們可以一步一步來做這些事情,也可以用stream來做。
如果我們可以一個(gè)bit一個(gè)bit地處理response,圖片局部可以渲染得更快,甚至可以整張圖片都可以渲染得更快,因?yàn)閳D片還在請(qǐng)求中的時(shí)候就可以被并行地解析。這就是stream。
我們用事件也可以做類似的事情,但是用stream的好處是
- start和end可被感知(雖然stream可能是無限長(zhǎng)的)
- 緩存還未被讀取的數(shù)據(jù)(用event的話事件發(fā)生之前的數(shù)據(jù)我們讀不到,stream是源源不斷的,event是節(jié)點(diǎn)型的)
- stream鏈(通過pipe來實(shí)現(xiàn)stream序列)
- 內(nèi)置的錯(cuò)誤處理機(jī)制(錯(cuò)誤可以在pipe中傳遞)
- 可取消的,并且被取消的數(shù)據(jù)可以備份
- 流控制,我們可以根據(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ò):
- 服務(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的特性。


可以看到,性能提升了不少,但是還是比不上服務(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加service worker意味著你可以瞬間渲染首屏內(nèi)容,然后用小于或等于服務(wù)端渲染的時(shí)間(請(qǐng)求的內(nèi)容比服務(wù)端渲染還少)來渲染余下的內(nèi)容。內(nèi)容渲染還是用的原生的瀏覽器的HTML parser,不會(huì)像innerHTML那樣丟失一些特性。
除了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)的能力,比如
- Gzip/壓縮
- 音視頻解碼
- 圖片解碼
- HTML/XML parser
現(xiàn)在說這些還有點(diǎn)早,但是如果你想在stream上面開發(fā)一些自己的API,reference implementation這是一些pollyfill。
流是瀏覽器很有價(jià)值一個(gè)特性,并且在2016年會(huì)對(duì)javascript解鎖!