译者的话(也就是mandy的话):
stream不是一个新的概念,浏览器一直都在使用stream,但是并没有暴露给前端开发者,我们也因此错过了一个巨大的宝藏。好在一些新的标准和浏览器实现中,我们可以染指stream了。
译文夹带着个人理解,如果想深入研究请联系译者或移步原文the year of web streams
好吧,在一月份就断言今年是某个东西的一年有点草率,但是web stream的潜能真的让我很兴奋。
streams可以用来做一些有趣的事情,比如,“把云换成屁股”(一个恶趣味又无语的项目https://github.com/panicsteve/cloud-to-butt), 又比如把MPEG转码成GIF。但是最重要的,streams结合service workers,可以使前端渲染性能,提高一个level。
streams适合用来做什么?
假设我们要拉取和显示一张图片,步骤是这样的
- 通过网络拉取图片
- 解析图片,把图片数据处理成可以渲染的像素数据。
- 渲染图片
我们可以一步一步来做这些事情,也可以用stream来做。
如果我们可以一个bit一个bit地处理response,图片局部可以渲染得更快,甚至可以整张图片都可以渲染得更快,因为图片还在请求中的时候就可以被并行地解析。这就是stream。
我们用事件也可以做类似的事情,但是用stream的好处是
- start和end可被感知(虽然stream可能是无限长的)
- 缓存还未被读取的数据(用event的话事件发生之前的数据我们读不到,stream是源源不断的,event是节点型的)
- stream链(通过pipe来实现stream序列)
- 内置的错误处理机制(错误可以在pipe中传递)
- 可取消的,并且被取消的数据可以备份
- 流控制,我们可以根据播放速度来控制数据接收速度。
最后一点很重要,想象一下,我们用stream来接收和播放一个视屏,如果我们一秒钟能下载和解析200帧的视频, 而只想一秒钟播放24帧,那么我们很容易就积压太多的帧数而耗尽内存。
所以有了流控制的概念,渲染stream
从解码stream中一秒钟拉取24帧,解码stream
发现自己解码的速度比渲染快,就会慢下来。然后请求stream
发现自己请求数据的速度比解码stream
快,也会跟着慢下来。
因为stream和播放器的强关联,一个stream只能对接一个播放器,不过,未被解析的stream可以有旁路,这种情况下,这个被分叉的stream可以充当缓存数据的角色。
浏览器本身就是用streams来接收数据的,我们看到浏览器上的页面/图片/视频可以一点一点逐步就显示,都是streams的功劳。这足以体现streams的优异之处,然而直到最近,在standardisation effort的贡献下,我们终于可以用js操作这些streams。
Streams + the fetch API
fetch spec中定义了response 对象,可以暴露各种各样的格式的属性,responese.body就是其中一个,我们就是通过这个属性来接触底层的streams。最新版本的chrome已经支持responese.body
假如我们想拿到response的contrnt-length,即使不通过header,也不用把整个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,但是这么庞大的体积不会存在内存中,我们只记录内容的大小,内容本身会被gc掉,这就是streams的优势。
result.velue可能是stream创建者创建的任何格式,在上面的例子中是一个二进制数据,如果你想转成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字符被截断的时候,decoder会缓存上一个数据,知道整个被截断的字符可以被解析出来。比如像 ♥ 这样的字符是三字节的,可能发生截断。
TextDecoder
现在看来有些笨拙,但它最有希望成为将来的transform stream(只要浏览器端有这个定义)。一个transform stream同时拥有writeable stream和readable stream,它用writeable stream来处理数据,然后下一个stream可以读它的readable stream。用transform stream来实现上面的例子:
var reader = response.body
.pipeThrough(new TextDecoder()).getReader();
reader.read().then(result => {
// result.value will be a string
});
这是浏览器以后应该做的优化,毕竟response stream和TextDecoder transform stream 都是浏览器提供的。
取消一个fetch
用stream.cancel()
或者response.body.cancel()
就可以取消一个fetch行为,fetch就会响应的取消download。
View demo
这个demo搜索一个大的html文件,一旦找到了匹配的内容,整个请求就取消了,几乎不用什么内存。
看到这里是不是觉得有点意思了,不过,这些都是2015年的东西了,2016,不只这些
创建自己的readable stream
如果开启chrome的Experimental web platform features,你可以创建自己的readable stream。
var stream = new ReadableStream({
start(controller) {},
pull(controller) {},
cancel(reason) {}
}, queuingStrategy);
举个例子,比如我们要持续输出一个random一个数字,到随机数大于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);
}
});
demo演示. Note: 需要把chrome的chrome://flags/#enable-experimental-web-platform-features
打开
你可以自己决定在什么时候传值给controller.enqueue,当你有数据要传的时候再调用就可以了。
慢速吐出一个string
先上效果.Note: 需要把chrome的chrome://flags/#enable-experimental-web-platform-features
打开
你可以看到一个缓慢渲染HTML的页面(故意的),这个response整个都是在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();
// 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'}
});
});
用一个stream接收多种资源,减少页面渲染时间
这恐怕是最实用的运用场景了。对前端性能有极大的提升。
几个月前我写了一个首屏离线化的demo 我当时的目标是写一个出色的web app,它必须有很快的响应速度,于是渐进增强地用了service worker的一些特性。
给出一些当时的数据,osx的模拟器下,3g网络:
- 服务端渲染
数据还不错,然后我加入了一些service worker的特性来进一步优化:
首屏渲染时间减少了很多,但是首屏之后的渲染反而退步了。
最快的方法当然缓存整个页面,但是这样子太浪费内存了。所以我缓存了头部,css和js,这样可以使首屏速度最快。然后拉取内容,用js来渲染。这也是大部分web应用的做法: 客户端渲染。
无论用直接的网络请求还是用service worker,HTML都是边下载边渲染的,但是我用js来请求内容,再用innerHTML插入内容,整个页面是等数据回来才开始渲染的,这也是上图相比服务端渲染慢2秒的原因。
内容越大,比服务端渲染的速度就越慢。
这就是我抱怨服务端渲染的web app或者框架的原因,它们从一开始就抛弃了 stream,造成了这么差的性能。
于是我想通过伪stream来挽回一些性能,做法十分hacky,页面用fetch来拉取内容,并且用stream来读取内容,每次独到的内容有9k,便用innerHTML渲染出来,这样来模拟浏览器逐渐渲染html的特性。
可以看到,性能提升了不少,但是还是比不上服务端渲染,而且,用innerHTML来插入标签的方法跟原生渲染标签还是有区别的,最明显的,innerHTML的script标签里的代码不被执行.
这时候真正的stream来了,不同于提供一个空壳,然后用js来填充内容,我让service worker构建一个stream,这个stream会把渲染好的页面吐给浏览器(头部依旧是来自缓存,内容依旧从网上下载),这就像是服务端渲染,不过是在service worker里进行的。
用stream加service worker意味着你可以瞬间渲染首屏内容,然后用小于或等于服务端渲染的时间(请求的内容比服务端渲染还少)来渲染余下的内容。内容渲染还是用的原生的浏览器的HTML parser,不会像innerHTML那样丢失一些特性。
除了readable stream,其他stream还在开发中,但是我们可以做的事情已经很不可思议了,如果你想提高一个重内容型的web应用而且想实现首屏离线化,又不想重构你的代码,那么用stream加service worker是简单的办法。
在web中有一个原生stream支持意味着我们可以染指浏览器各种stream相关的能力,比如
- Gzip/压缩
- 音视频解码
- 图片解码
- HTML/XML parser
现在说这些还有点早,但是如果你想在stream上面开发一些自己的API,reference implementation这是一些pollyfill。
流是浏览器很有价值一个特性,并且在2016年会对javascript解锁!