2016,属于web stream的一年

译者的话(也就是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适合用来做什么?

假设我们要拉取和显示一张图片,步骤是这样的

  1. 通过网络拉取图片
  2. 解析图片,把图片数据处理成可以渲染的像素数据。
  3. 渲染图片

我们可以一步一步来做这些事情,也可以用stream来做。

如果我们可以一个bit一个bit地处理response,图片局部可以渲染得更快,甚至可以整张图片都可以渲染得更快,因为图片还在请求中的时候就可以被并行地解析。这就是stream。

我们用事件也可以做类似的事情,但是用stream的好处是

  1. start和end可被感知(虽然stream可能是无限长的)
  2. 缓存还未被读取的数据(用event的话事件发生之前的数据我们读不到,stream是源源不断的,event是节点型的)
  3. stream链(通过pipe来实现stream序列)
  4. 内置的错误处理机制(错误可以在pipe中传递)
  5. 可取消的,并且被取消的数据可以备份
  6. 流控制,我们可以根据播放速度来控制数据接收速度。

最后一点很重要,想象一下,我们用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网络:

  1. 服务端渲染

服务端渲染

数据还不错,然后我加入了一些service worker的特性来进一步优化:


前端渲染

首屏渲染时间减少了很多,但是首屏之后的渲染反而退步了。

最快的方法当然缓存整个页面,但是这样子太浪费内存了。所以我缓存了头部,css和js,这样可以使首屏速度最快。然后拉取内容,用js来渲染。这也是大部分web应用的做法: 客户端渲染。

无论用直接的网络请求还是用service worker,HTML都是边下载边渲染的,但是我用js来请求内容,再用innerHTML插入内容,整个页面是等数据回来才开始渲染的,这也是上图相比服务端渲染慢2秒的原因。
内容越大,比服务端渲染的速度就越慢。

这就是我抱怨服务端渲染的web app或者框架的原因,它们从一开始就抛弃了 stream,造成了这么差的性能。

于是我想通过伪stream来挽回一些性能,做法十分hacky,页面用fetch来拉取内容,并且用stream来读取内容,每次独到的内容有9k,便用innerHTML渲染出来,这样来模拟浏览器逐渐渲染html的特性。


伪stream

可以看到,性能提升了不少,但是还是比不上服务端渲染,而且,用innerHTML来插入标签的方法跟原生渲染标签还是有区别的,最明显的,innerHTML的script标签里的代码不被执行.

这时候真正的stream来了,不同于提供一个空壳,然后用js来填充内容,我让service worker构建一个stream,这个stream会把渲染好的页面吐给浏览器(头部依旧是来自缓存,内容依旧从网上下载),这就像是服务端渲染,不过是在service worker里进行的。


真stream

用stream加service worker意味着你可以瞬间渲染首屏内容,然后用小于或等于服务端渲染的时间(请求的内容比服务端渲染还少)来渲染余下的内容。内容渲染还是用的原生的浏览器的HTML parser,不会像innerHTML那样丢失一些特性。

附上对比视频

除了readable stream,其他stream还在开发中,但是我们可以做的事情已经很不可思议了,如果你想提高一个重内容型的web应用而且想实现首屏离线化,又不想重构你的代码,那么用stream加service worker是简单的办法。

在web中有一个原生stream支持意味着我们可以染指浏览器各种stream相关的能力,比如

  1. Gzip/压缩
  2. 音视频解码
  3. 图片解码
  4. HTML/XML parser
    现在说这些还有点早,但是如果你想在stream上面开发一些自己的API,reference implementation这是一些pollyfill。

流是浏览器很有价值一个特性,并且在2016年会对javascript解锁!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容