前端黑科技:美团网页首帧优化实践

美团支付前端团队支持着美团钱包及支付业务,涉及项目众多,并且项目迭代很快,挑战巨大。在总结经验之后,我们举办了第40期美团技术沙龙(点击查看PPT及视频资料),与大家分享我们的实战经验。

11月24日下午,《美团技术沙龙第45期:如何构建高性能、稳定的后端服务系统》将在北京朝阳区望京恒电大厦C座美团点评北京总部1层恒基咖啡举办,我们结合美团大流量应用的典型业务场景,从后台系统架构的演进、高性能的服务计算、以及稳定性保障等方面的实践应用展开分享。更多活动详情请戳>>活动报名链接

(图片来自寒阳分享现场)

自JavaScript诞生以来,前端技术发展非常迅速。移动端白屏优化是前端界面体验的一个重要优化方向,Web 前端诞生了 SSR
、CSR、预渲染等技术。在美团支付的前端技术体系里,通过预渲染提升网页首帧优化,从而优化了白屏问题,提升用户体验,并形成了最佳实践。

在前端渲染领域,主要有以下几种方式可供选择:

通过对比,同构方案集合 CSR 与 SSR 的优点,可以适用于大部分业务场景。但由于在同构的系统架构中,连接前后端的 Node
中间层处于核心链路,系统可用性的瓶颈就依赖于 Node ,一旦作为短板的 Node 挂了,整个服务都不可用。

结合到我们团队负责的支付业务场景里,由于支付业务追求极致的系统稳定性,服务不可用直接影响到客诉和资损,因此我们采用浏览器端渲染的架构。在保证系统稳定性的前提下,还需要保障用户体验,所以采用了预渲染的方式。

那么究竟什么是预渲染呢?什么是 FCP/FMP 呢?我们先从最常见的 CSR 开始说起。

以 Vue 举例,常见的 CSR 形式如下:

一切看似很美好。然而,作为以用户体验为首要目标的我们发现了一个体验问题:首屏白屏问题。

为什么会首屏白屏

浏览器渲染包含 HTML 解析、DOM 树构建、CSSOM 构建、JavaScript 解析、布局、绘制等等,大致如下图所示:

要搞清楚为什么会有白屏,就需要利用这个理论基础来对实际项目进行具体分析。通过 DevTools 进行分析:

  • 等待 HTML 文档返回,此时处于白屏状态。

  • 对 HTML 文档解析完成后进行首屏渲染,因为项目中对加了灰色的背景色,因此呈现出灰屏。

  • 进行文件加载、JS 解析等过程,导致界面长时间出于灰屏中。

  • 当 Vue 实例触发了 mounted 后,界面显示出大体框架。

  • 调用 API 获取到时机业务数据后才能展示出最终的页面内容。

由此得出结论,因为要等待文件加载、CSSOM 构建、JS
解析等过程,而这些过程比较耗时,导致用户会长时间出于不可交互的首屏灰白屏状态,从而给用户一种网页很“慢”的感觉。那么一个网页太“慢”,会造成什么影响呢?

“慢”的影响

Global Web Performance Matters for
ecommerce

的报告中指出:

  • 57%的用户更在乎网页在3秒内是否完成加载。

  • 52%的在线用户认为网页打开速度影响到他们对网站的忠实度。

  • 每慢1秒造成页面 PV 降低11%,用户满意度也随之降低降低16%。

  • 近半数移动用户因为在10秒内仍未打开页面从而放弃。

我们团队主要负责美团支付相关的业务,如果网站太慢会影响用户的支付体验,会造成客诉或资损。既然网站太“慢”会造成如此重要的影响,那要如何优化呢?

优化思路

在[User-centric Performance
Metrics](https://developers.google.com/web/fundamentals/performance/user-
centric-performance-metrics)一文中,共提到了4个页面渲染的关键指标:

基于这个理论基础,再回过头来看看之前项目的实际表现:

可见在 FP 的灰白屏界面停留了很长时间,用户不清楚网站是否有在正常加载,用户体验很差。

试想:如果我们可以将 FCP 或 FMP 完整的 HTML 文档提前到 FP
时机预渲染,用户看到页面框架,能感受到页面正在加载而不是冷冰冰的灰白屏,那么用户更愿意等待页面加载完成,从而降低了流失率。并且这种改观在弱网环境下更明显。

通过对比 FP、FCP、FMP 这三个时期 DOM 的差异,发现区别在于:

  • FP:仅有一个 div 根节点。

  • FCP:包含页面的基本框架,但没有数据内容。

  • FMP:包含页面所有元素及数据。

仍然以 Vue 为例, 在其生命周期中,mounted 对应的是 FCP,updated 对应的是 FMP。那么具体应该使用哪个生命周期的 HTML
结构呢?

通过以上的对比,最终选择在 mounted 时触发构建时预渲染。由于我们采用的是 CSR 的架构,没有 Node 作为中间层,因此要实现 DOM
内容的预渲染,就需要在项目构建编译时完成对原始模板的更新替换。

至此,我们明确了构建时预渲染的大体方案。

构建时预渲染方案

构建时预渲染流程:

配置读取

由于 SPA 可以由多个路由构成,需要根据业务场景决定哪些路由需要用到预渲染。因此这里的配置文件主要是用于告知编译器需要进行预渲染的路由。

在我们的系统架构里,脚手架是基于 Webpack 自研的,在此基础上可以自定义自动化构建任务和配置。

触发构建

项目中主要是使用 TypeScript,利用 TS
装饰器,我们封装了统一的预渲染构建的钩子方法,从而只用一行代码即可完成构建时预渲染的触发。

装饰器:

使用:

构建编译

从流程图上,需要在发布机上启动模拟的浏览器环境,并通过预渲染的事件钩子获取当前的页面内容,生成最终的 HTML 文件。

由于我们在预渲染上的尝试比较早,当时还没有 Headless
Chrome

PuppeteerPrerender SPA
Plugin
等,因此在选型上使用的是
[phantomjs-prebuilt](https://www.npmjs.com/package/phantomjs-
prebuilt)(Prerender SPA Plugin 早期版本也是基于 phantomjs-prebuilt 实现的)。

通过 phantom 提供的 API 可获得当前 HTML,示例如下:

为了提高构建效率,并行对配置的多个页面或路由进行预渲染构建,保证在 5S 内即可完成构建,流程图如下:

方案优化

理想很丰满,现实很骨感。在实际投产中,构建时预渲染方案遇到了一个问题。

我们梳理一下简化后的项目上线过程:

开发 -> 编译 -> 上线

假设本次修改了静态文件中的一个 JS 文件,这个文件会通过 CDN 方式在 HTML 里引用,那么最终在 HTML 文档中的引用方式是 <script src="http://cdn.com/index.js"></script>。然而由于项目还没有上线,所以其实通过完整 URL
的方式是获取不到这个文件的;而预渲染的构建又是在上线动作之前,所以问题就产生了:

构建时预渲染无法正常获取文件,导致编译报错

怎么办?

请求劫持

因为在做预渲染时,我们使用启动了一个模拟的浏览器环境,根据 phantom 提供的 API,可以对发出的请求加以劫持,将获取 CDN
文件的请求劫持到本地,从而在根本上解决了这个问题。示例代码如下:

构建时预渲染研发流程及效果

最终,构建时预渲染研发流程如下:

开发阶段:

  • 通过 TypeScript 的装饰器单行引入预渲染构建触发的方法。

  • 发布前修改编译构建的配置文件。

发布阶段:

  • 先进行常规的项目构建。

  • 若有预渲染相关配置,则触发预渲染构建。

  • 通过预渲染得到最终的文件,并完成发布上线动作。

完整的用户请求路径如下:

通过构建时预渲染在项目中的使用,FCP 的时间相比之前减少了 75%。

RxJS

1. 前言

1.1 什么是RxJS?

RxJS是ReactiveX编程理念的JavaScript版本。ReactiveX来自微软,它是一种针对异步数据流的编程。简单来说,它将一切数据,包括HTTP请求,DOM事件或者普通数据等包装成流的形式,然后用强大丰富的操作符对流进行处理,使你能以同步编程的方式处理异步数据,并组合不同的操作符来轻松优雅的实现你所需要的功能。

1.2 RxJS可用于生产吗?

ReactiveX由微软于2012年开源,目前各语言库由ReactiveX组织维护。RxJS在GitHub上已有8782个star,目前最新版本为5.5.2,并持续开发维护中,其中官方测试用例共计2699个。

1.3 RxJS对项目代码的影响?

RxJS中的流以Observable对象呈现,获取数据需要订阅Observable,形式如下:

1
2
3
4
5
const ob = http$.getSomeList(); //getSomeList()返回某个由`Observable`包装后的http请求

ob.subscribe((data) =>console.log(data));

`//在变量末尾加$表示Observable类型的对象。`

以上与Promise类似:

1
2
3
const promise = http.getSomeList(); // 返回由`Promise`包装的http请求

promise.then((data) =>console.log(data));

实际上Observable可以认为是加强版的Promise,它们之间是可以通过RxJS的API互相转换的:

1
2
3
const ob = Observable.fromPromise(somePromise); // Promise转为Observable

const promise = someObservable.toPromise(); // Observable转为Promise

因此可以在Promise方案的项目中安全使用RxJS,并能够随时升级到完整的RxJS方案。

1.4 RxJS会增加多少体积?

RxJS(v5)整个库压缩后约为140KB,由于其模块化可扩展的设计,因此仅需导入所用到的类与操作符即可。导入RxJS常用类与操作符后,打包后的体积约增加30-60KB,具体取决于导入的数量。

不要用 import { Observable } from ‘rxjs’这种方式导入,这会导入整个rxjs库,按需导入的方式如下:

import { Observable } from ‘rxjs/Observable’ //导入类

import ‘rxjs/add/operator/map’ // 导入实例操作符

import ‘rxjs/add/observable/forkJoin’ // 导入类操作符

2. RxJS快速入门

2.1 初级核心概念

  • Observable

  • Observer

  • Operator

Observable被称为可观察序列,简单来说数据就在Observable中流动,你可以使用各种operator对流进行处理,例如:

1
2
3
const ob = Observable.interval(1000);

ob.take(3).map(n => n * 2).filter(n => n > 2);

第一步代码我们通过类方法interval创建了一个Observable序列,ob作为源会每隔1000ms发射一个递增的数据,即0 -> 1 ->
2。第二步我们使用操作符对流进行处理,take(3)表示只取源发射的前3个数据,取完第三个后关闭源的发射;map表示将流中的数据进行映射处理,这里我们将数据翻倍;filter表示过滤掉出符合条件的数据,根据上一步map的结果,只有第二和第三个数据会留下来。

上面我们已经使用同步编程创建好了一个流的处理过程,但此时ob作为源并不会立刻发射数据,如果我们在map中打印n是不会得到任何输出的,因为ob作为Observable序列必须被“订阅”才能够触发上述过程,也就是subscribe(发布/订阅模式)。

1
2
3
const ob = Observable.interval(1000);

ob.take(3).map(n => n * 2).filter(n => n > 0).subscribe(n =>console.log(n));

结果:

2 //第2秒

4 //第3秒

上面代码中我们给subscribe传入了一个函数,这其实是一种简写,subscribe完整的函数签名如下:

1
2
3
4
5
6
7
8
9
ob.subscribe({

next: d => console.log(d),

error: err => console.error(err),

complete: () => console.log('end of the stream')

})

直接给subscribe传入一个函数会被当做是next函数。这个完整的包含3个函数的对象被称为observer(观察者),表示的是对序列结果的处理方式。next表示数据正常流动,没有出现异常;error表示流中出错,可能是运行出错,http报错等等;complete表示流结束,不再发射新的数据。在一个流的生命周期中,error和complete只会触发其中一个,可以有多个next(表示多次发射数据),直到complete或者error。

observer.next可以认为是Promise中then的第一个参数,observer.error对应第二个参数或者Promise的catch。

RxJS同样提供了catch操作符,err流入catch后,catch必须返回一个新的Observable。被catch后的错误流将不会进入observer的error函数,除非其返回的新observable出错。

1
2
3
4
5
6
7
Observable.of(1).map(n => n.undefinedMethod()).catch(err => {

// 此处处理catch之前发生的错误

return Observable.of(0); // 返回一个新的序列,该序列成为新的流。

});

2.2 创建可观察序列

创建一个序列有很多种方式,我们仅列举常用的几种:

Observable.of(…args)

Observable.of()可以将普通JavaScript数据转为可观察序列,[点我测试](http://xgrommx.github.io/rx-
book/content/observable/observable_methods/of.html)。

Observable.fromPromise(promise)

将Promise转化为Observable,[点我测试](http://xgrommx.github.io/rx-
book/content/observable/observable_methods/frompromise.html)。

Observable.fromEvent(elment, eventName)

从DOM事件创建序列,例如Observable.fromEvent($input,
‘click’),[点我测试](http://xgrommx.github.io/rx-
book/content/observable/observable_methods/fromevent.html)。

Observable.ajax(url | AjaxRequest)

发送http请求,AjaxRequest参考这里

Observable.create(subscribe)

这个属于万能的创建方法,一般用于只提供了回调函数的某些功能或者库,在你用这个方法之前先想想能不能用RxJS上的类方法来创建你所需要的序列,[点我测试](http://xgrommx.github.io
/rx-book/content/observable/observable_methods/create.html)。

2.3 合并序列

合并序列也属于创建序列的一种,例如有这样的需求:进入某个页面后拿到了一个列表,然后需要对列表每一项发出一个http请求来获取对应的详细信息,这里我们把每个http请求作为一个序列,然后我们希望合并它们。

合并有很多种方式,例如N个请求按顺序串行发出(前一个结束再发下一个);N个请求同时发出并且要求全部到达后合并为数组,触发一次回调;N个请求同时发出,对于每一个到达就触发一次回调。

如果不用RxJS,我们会比较难处理这么多情形,不仅实现麻烦,维护更麻烦,下面是使用RxJS对上述需求的解决方案:

1
2
3
4
5
6
7
8
9
const ob1 = Observable.ajax('api/detail/1');

const ob2 = Observable.ajax('api/detail/2');

...

const obs = [ob1, ob2...];

`// 分别创建对应的HTTP请求。`
  1. N个请求按顺序串行发出(前一个结束再发下一个)

Observable.concat(…obs).subscribe(detail =>console.log(‘每个请求都触发回调’));

  1. N个请求同时并行发出,对于每一个到达就触发一次回调

Observable.merge(…obs).subscribe(detail =>console.log(‘每个请求都触发回调’));

  1. N个请求同时发出并且要求全部到达后合并为数组,触发一次回调

Observable.forkJoin(…obs).subscribe(detailArray =>console.log(‘触发一次回调’));

3. 使用RxJS实现搜索功能

搜索是前端开发中很常见的功能,一般是监听的keyup事件,然后将内容发送到后台,并展示后台返回的数据。

<inputid=”text”>`

<script>

var text = document.querySelector('#text');

text.addEventListener('keyup', (e) =>{

var searchText = e.target.value;

// 发送输入内容到后台

$.ajax({

url:/search/${searchText},

success: data => {

// 拿到后台返回数据,并展示搜索结果

render(data);

}

});

});

</script>

上面代码实现我们要的功能,但存在两个较大的问题:

  • 多余的请求

当想搜索“爱迪生”时,输入框可能会存在三种情况,“爱”、“爱迪”、“爱迪生”。而这三种情况将会发起 3 次请求,存在 2 次多余的请求。

  • 已无用的请求仍然执行

一开始搜了“爱迪生”,然后马上改搜索“达尔文”。结果后台返回了“爱迪生”的搜索结果,执行渲染逻辑后结果框展示了“爱迪生”的结果,而不是当前正在搜索的“达尔文”,这是不正确的。

减少多余请求数,可以用 setTimeout 函数节流的方式来处理,核心代码如下:

<inputid=”text”>`

<script>

var text = document.querySelector('#text'),

timer = null;

text.addEventListener('keyup', (e) =>{

// 在 250 毫秒内进行其他输入,则清除上一个定时器

clearTimeout(timer);

// 定时器,在 250 毫秒后触发

timer = setTimeout(() => {

console.log('发起请求..');

},250)

})

</script>

已无用的请求仍然执行
的解决方式,可以在发起请求前声明一个当前搜索的状态变量,后台将搜索的内容及结果一起返回,前端判断返回数据与当前搜索是否一致,一致才走到渲染逻辑。最终代码为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
<inputid="text"></input>`

`<script>`

`var text = document.querySelector('#text'),`

` timer = null,`

` currentSearch = '';`



` text.addEventListener('keyup', (e) =>{`

` clearTimeout(timer)`

` timer = setTimeout(() => {`

` // 声明一个当前所搜的状态变量`

` currentSearch = '书'; `



`var searchText = e.target.value;`

` $.ajax({`

`url: `/search/${searchText}`,`

`success: data => {`

` // 判断后台返回的标志与我们存的当前搜索变量是否一致`

`if (data.search === currentSearch) {`

` // 渲染展示`

` render(data);`

` } else {`

` // ..`

` }`

` } `

` });`

` },250)`

` })`

`</script>`

上面代码基本满足需求,但代码开始显得乱糟糟。我们来使用RxJS实现上面代码功能,如下:

1
2
3
4
5
6
7
8
9
10
11
var text = document.querySelector('#text');

var inputStream = Rx.Observable.fromEvent(text, 'keyup') //为dom元素绑定'keyup'事件

.debounceTime(250) // 防抖动

.pluck('target', 'value') // 取值

.switchMap(url => Http.get(url)) // 将当前输入流替换为http请求

.subscribe(data => render(data)); // 接收数据

RxJS能简化你的代码,它将与流有关的内部状态封装在流中,而不需要在流外定义各种变量来以一种上帝视角控制流程。Rx的编程方式使你的业务逻辑流程清晰,易维护,并显著减少出bug的概率。

个人总结的常用操作符:

类操作符(通常为合并序列或从已有数据创建序列)

合并forkJoin, merge, concat

创建of, from, fromPromise, fromEvent, ajax, throw

实例操作符(对流中的数据进行处理或者控制流程)

map, filter,switchMap, toPromise, catch, take, takeUntil, timeout,
debounceTime, distinctUntilChanged, pluck。

对于这些操作符的使用不再详细描述,请参阅网上资料。

中文官网 http://cn.rx.js.org/

附上个人翻译的一些文章

参考文章:构建流式应用:RxJS 详解


RxJS 6有哪些新变化?

RxJs 6于2018年4月24日正式发布,为开发人员带来了一些令人兴奋的增补和改进。Ben Lesh, rxJS核心开发成员,强调:

  1. RxJS 6在拥有更小API的同时,带来了更整洁的引入方式

  2. 提供一个npm包,该package可以处理RxJS的向后兼容性,使得开发人员可以在不更改代码的情况下进行更新,同时还可以帮助TypeScript代码自动迁移。

RxJs
6这些新的改动为开发人员提供了以下三方面的优化:模块化方面的改进、性能提升、调试更方便。RxJs团队尽力保持新版本的向后兼容性,但是为了减少RxJs的API数量,还是引入了一些重大修改。

下面让我们一起来看一下RxJs团队在新版本中引入了哪些修改。

RxJS 6的向后兼容性

为了便捷地从RxJS 5迁移到RxJS 6,RxJS团队发布了一个名为rxjs-compat的兄弟软件包。该软件包在v6和v5的API之间创建了一个兼容层。

RxJs团队建议开发人员通过安装^6.0.0版本的rxjs和rxjs-compat包来升级现有应用:

npm install rxjs@6 rxjs-compat@6 --save

此包允许您在升级RxJS 6的同时继续运行现有代码库,而不会出现问题。他支持在RxJs 6中移除掉的功能。

安装rxjs-compat会导致打包后代码包体积的增加,如果你使用的是4.0.0版本以下的Webpack,该影响会被放大。

因此建议升级完成后将rxjs-compat移除。

使用rxjs-compat升级RxJS的限制

只有两个重大修改在rxjs-compat中未覆盖:

TypeScript原型操作符

在极少数情况下,您的代码库定义了它自己的TypeScript原型操作符并修改了Observable命名空间。该情况下,您需要更新你的操作符相关代码才能使TypeScript正常编译。

在版本发布说明中,用户自定义的原型操作符可按如下方式创建:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Observable.prototype.userDefined = () => {

return new Observable((subscriber) => {

this.subscribe({

next(value) { subscriber.next(value); },

error(err) { subscriber.error(err); },

complete() { [subscriber.complete();](http://subscriber.complete\(\);/) },

});

});

});



source$.userDefined().subscribe();

为编译该类型的自定义操作符,需要做如下修改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

const userDefined = <T>() => (source: Observable<T>) => new
Observable<T>((subscriber) => {

this.subscribe({

next(value) { subscriber.next(value); },

error(err) { subscriber.error(err); },

complete() { [subscriber.complete();](http://subscriber.complete\(\);/) },

});

});

});



source$.pipe(

userDefined(),

)

同步错误处理

不再支持在try /
catch块内调用Observable.subscribe()。使用用Observable.subscribe()方法中的错误回调方法替换原先的try /
catch块来完成的异步错误的处理。

示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// deprecated

try {

source$.subscribe(nextFn, undefined, completeFn);

} catch (err) {

handleError(err);

}



// use instead

source$.subscribe(nextFn, handleError, completeFn);

现在在Observable.subscribe()中必须定义一个错误回调方法来异步处理错误。

删除RxJs兼容层前需要做的修改

如上所诉,rxjs-compat提供了V5与v6API间的临时兼容层,实质上rxjs-
compat为您的代码库提供了所需的v5版本功能,使得您可以逐步将您的代码库升级到v6版本。为了完成升级并移除rxjs-
compat依赖,您的代码库需要重构并停止使用v5版本中的如下功能:

修改import路径

建议TypeScript开发人员使用rxjs-tslint来重构import路径。

RxJS团队设计了以下规则来帮助JavaScript开发人员重构import路径:

  • rxjs: 包含创建方法,类型,调度程序和工具库。

import { Observable, Subject, asapScheduler, pipe, of, from, interval, merge, fromEvent } from'rxjs';

  • rxjs/operators: 包含所有的管道操作符

import { map, filter, scan } from 'rxjs/operators';

  • rxjs/webSocket: 包含websocket subject实现.

import { webSocket } from'rxjs/webSocket';

  • rxjs/ajax: 包含Rx ajax实现.

import { ajax } from'rxjs/ajax';

  • rxjs/testing: 包含RxJS的测试工具库.

import { TestScheduler } from'rxjs/testing';

以下是一项小调查:您是否有常识使用rxjs-tslint升级您的应用程序?

使用管道操作而不是链式操作

使用新的管道操作符语法替换旧有的链式操作。上一个操作符方法的结果会被传递到下一个操作符方法中。

不要移除rxjs-compat包,直到你将所有的链式操作修改为管道操作符。如果您使用TypeScript, ts-lint会在某种程度上自动执行此项重构。

Ben Lesh在[ng-conf 2018](https://www.ng-conf.org/sessions/introducing-
rxjs6/)上解释了为什么我们应该使用管道操作符

请按照如下步骤将您的链式操作替换为管道操作:

  • 从rxjs-operators中引入您需要的操作符

注意:由于与Javascript保留字冲突,以下运算符名字做了修改:do -> tap, catch ->

catchError, switch -> switchAll, finally -> finalize

import { map, filter, catchError, mergeMap } from ‘rxjs/operators’;

  • 使用pipe()包裹所有的操作符方法。确保所有操作符间的.被移除,转而使用,连接。记住!!!有些操作符的名称变了!!!

以下为升级示例:

// an operator chain

source

.map(x => x + x)

.mergeMap(n =>of(n + 1, n + 2)

.filter(x => x % 1 == 0)

.scan((acc, x) => acc + x, 0)

)

.catch(err =>of(‘error found’))

.subscribe(printResult);

// must be updated to a pipe flow

source.pipe(

map(x => x + x),

mergeMap(n =>of(n + 1, n + 2).pipe(

filter(x => x % 1 == 0),

scan((acc, x) => acc + x, 0),

)),

catchError(err =>of(‘error found’)),

).subscribe(printResult);

注意我们在以上代码中嵌套使用了pipe()。

使用函数而不是类

使用函数而不是类来操作可观察对象(Observables)。所有的Observable类已被移除。他们的功能被新旧操作符及函数替代。这些替代品的功能与之前的类功能一模一样。

示例如下:

// removed

ArrayObservable.create(myArray)

// use instead

from(myArray)

// you may also use

newoperatorfromArray().

有关替换v5类为v6函数的完整列表,请查看RxJS文档

特殊情况

  • ConnectableObservable在v6中不能直接使用,要访问它,请使用操作符multicast,publish,publishReplay和publishLast。

  • SubscribeOnObservable在v6中不能直接使用,要访问它,请使用操作符subscribeOn

移除resultSelector

Result Selectors是一项没有被广泛使用甚至没有文档说明的RxJs特性,同时Result
Selectors严重的增加了RxJs代码库的体积,因此RxJs团队决定弃用或删除他。

对于使用到该功能的开发人员,他们需要将esultSelector参数替换为外部代码。

对于first(), last()这两个函数,这些参数已被移除,在删除rxjs-compat之前务必升级代码。

对于其他拥有resultSelector参数的函数,如mapping操作符,该参数已被弃用,并

以其他方式重写。如果您移除rxjs-compat,这些函数仍可正常工作,但是RxJs团队声明他们必须在v7版本发布之前将其移除。

针对该情况的更多详情,请查阅RxJs文档

其他RxJs6弃用

Observable.if and Observable.throw

Observable.if已被iif()取代,Observable.throw已被throwError()取代。您可使用rxjs-
tslint将这些废弃的成员方法修改为函数调用。

代码示例如下:

OBSERVABLE.IF > IIF()

// deprecated

Observable.if(test, a$, b$);

// use instead

iif(test, a$, b$);

OBSERVABLE.ERROR > THROWERROR()

// deprecated

Observable.throw(newError());

//use instead

throwError(newError());

已弃用的方法

根据迁移指南,以下方法已被弃用或重构:

merge

import { merge } from’rxjs/operators’;

a$.pipe(merge(b$, c$));

// becomes

import { merge } from’rxjs’;

merge(a$, b$, c$);

concat

import { concat } from ‘rxjs/operators’;

a$.pipe(concat(b$, c$));

// becomes

import { concat } from ‘rxjs’;

concat(a$, b$, c$);

combineLatest

import { combineLatest } from ‘rxjs/operators’;

a$.pipe(combineLatest(b$, c$));

// becomes

import { combineLatest } from ‘rxjs’;

combineLatest(a$, b$, c$);

race

import { race } from ‘rxjs/operators’;

a$.pipe(race(b$, c$));

// becomes

import { race } from ‘rxjs’;

race(a$, b$, c$);

zip

import { zip } from ‘rxjs/operators’;

a$.pipe(zip(b$, c$));

// becomes

import { zip } from ‘rxjs’;

zip(a$, b$, c$);

总结

RxJS 6带来了一些重大改变,但是通过添加rxjs-
compat软件包可以缓解这一问题,该软件包允许您在保持v5代码运行的同时逐渐迁移。对于Typescript用户,其他中包括大多数Angular开发人员,tslint提供了大量的自动重构功能,使转换变得更加简单。

任何升级与代码修改都会引入一些bug到代码库中。因此请务必测试您的功能以确保您的终端用户最终接受到相同的质量体验。

视频:RxJS 6详细介绍 by Ben Lesh

原文链接


RxJS 6发布,改进了性能和模块化

文章来源:infoqDylan Schiemann

RxJS团队宣布RxJS
6.0发布
。6.0改进了模块化方法和平滑迁移性能、为简化升级而添加了反向兼容软件包,并为TypeScript用户提供了代码迁移。

rxjs-compat软件包提供了一个版本间的兼容层,用于实现从RxJS 5到6的平滑迁移,

用户可以使用npm安装RxJS 6和兼容层,命令如下:

npm install rxjs@6 rxjs-compat@6--save

兼容层使代码无需更改即可升级到6。但如果开发人员想在部署到生产环境前降低RxJS源包的大小,还应于此后升级自身的源代码。

Angular 6用户也可受益于RxJS的Schematics。他们可以利用Angular 6提供的 ng update 机制在应用中自动安装rxjs-
compat。

RxJS 6将模块导入路径重新组织为如下几类:

  • rxjs: 创建方法、类型、调度器和工具。

  • rxjs/ajax: RxJS HTTP请求实现。

  • rxjs/operators: 可链式调用(Pipeable)的RxJS操作符。

  • rxjs/testing: RxJS测试工具。

  • rxjs/webSocket: RxJS WebSocket实现。

推荐RxJS的TypeScript用户使用 rxjs-tslint ,它有助于将版本5的导入路径重构为版本6。

RxJS
6的另一个显著改进,是将操作符转变为使用链式调用API。RxJS的前期版本中以对操作法原型打补丁的方式提供了链式调用,但这样的全局实现引入了一些挑战,包括对WebPack的摇树
(tree-shaking)优化功能,以及对代码检查(linting)工具。

例如,下面给出的例子代码使用了RxJS 5:

source

.map(x => x + x)

.mergeMap(n =>of(n +1, n +2)

.filter(x => x %1==0)

.scan((acc, x)=> acc + x,0)

)

.catch(err =>of(‘error found’))

.subscribe(printResult);

如果使用RxJS 6,那么代码变为:

source.pipe(

map(x => x + x),

mergeMap(n =>of(n +1, n +2).pipe(

filter(x => x %1==0),

scan((acc, x)=> acc + x,0),

)),

catchError(err =>of(‘error found’)),

).subscribe(printResult);

近期,RxJS项目牵头人Ben Lesh也谈及了RxJS
6
,并介绍了支持项目改进的动机所在。

RxJS使用Apache 2许可发布。更多信息,请访问RxJS网站。欢迎开发人员通过RxJS
GitHub项目
做出贡献。

查看英文原文:RxJS 6 Release Improves Performance and
Modularity

前端优化:9 个技巧,提高 Web 性能

##

OSC协作翻译

英文原文:Front End Optimization – 9 Tips to Improve Web Performance

链接:https://www.keycdn.com/blog/front-end-optimization/

译者:间_拾零, 边城, snake_007, physihan, Viyi

当今数字世界,存在着无数的网站,每天都需要处理各种不同的原因的访问。然而,这些网站中有很大一部分显得笨重,使用起来也很麻烦。没怎么优化的网站会被各种各样的问题困扰,包括加载时间、不支持移动设备、浏览器兼容性问题,等等。

这篇文章讲述可以帮助改善优化前端的技术,非常有用。主要内容有清理代码、压缩图片、压缩外部资源、使用
CDN,以及一些其它方法。这些方法会为你的网站带显著的速度提升和整体性能提升。

1. 清理 HTML 文档

HTML,即超文本标记语言,几乎是所有网站的支柱。HTML 为网页带来标题、子标题、列表和其它一些文档结构的格式。在最近更新的 HTML5
中,甚至可以创建图表。

HTML 很容易被网络爬虫识别,因此搜索引擎可以根据网站的内容在一定程度上实时更新。在写 HTML 的时候,你应该尝试让它简洁而有效。此外,在 HTML
文档中引用外部资源的时候也需要遵循一些最佳实践方法。

恰当放置 CSS

Web 设计者喜欢在网页建立起主要的 HTML 骨架之后再来创建样式表。这样一来,网页中的样式表往往会放在 HTML
的后面,接近文档结束的地方。然而推荐的做法是把 CSS 放在 HTML 的上面部分,文档头之内,这可以确保正常的渲染过程。

这个策略不能提高网站的加载速度,但它不会让访问者长时间看着空白屏幕或者无格式的文本(FOUT)等待。如果网页大部分可见元素已经加载出来了,访问者才更有可能等待加载整个页面,从而带来对前端的优化效果。这就是知觉性能。

正确放置 Javascript

另一方面,如果将 JavaScript 放置在 head 标签内或 HTML 文档的上部,这会阻塞 HTML 和 CSS
元素的加载过程。这个错误会导致页面加载时间增长,增加用户等待时间,容易让人感到不耐烦而放弃对网站的访问。不过,您可以通过将 JavaScript 属性置于
HTML 底部来避免此问题。

此外,在使用 JavaScript 时,人们通常喜欢用异步脚本加载。这会阻止script标签在 HTML 中的呈现过程,如,在文档中间的情况。

虽然对于网页设计师来说, HTML 是最值得使用的工具之一,但它通常要与 CSS 和 JavaScript 一起使用,这可能会导致网页浏览速度减慢。 虽然
CSS 和 JavaScript 有利于网页优化,但使用时也要注意一些问题。使用 CSS 和 JavaScript
时,要避免嵌入代码。因为当您嵌入代码时,要将 CSS 放置在样式标记中,并在脚本标记中使用 JavaScript,这会增加每次刷新网页时必须加载的 HTML
代码量。

绑定文件? 不用担心

在过去,你可能会频繁绑定 CSS 脚本到单个文件,以在 HTML 代码中引用外部文件。在使用 HTTP1.1
协议时,这是一项合理的实践,然而这一协议不再是必需的。

感谢 HTTP/2,现在你可以通过使用多路技术将单个 TCP 连接以异步方式收发 HTTP 请求和响应。

这意味着你不再需要频繁地将多个脚本绑定到单个文件。

2. 优化 CSS 性能

CSS,即级联样式表,能从 HTML 描述的内容生成专业而又整洁的文件。很多 CSS 需要通过 HTTP 请求来引入(除非使用内联
CSS),所以你要努力去除累赘的 CSS 文件,但要注意保留其重要特征。

如果你的 Banner、插件和布局样式是使用 CSS 保存在不同的文件内,那么,访问者的浏览器每次访问都会加载很多文件。虽然现在 HTTP/2
的存在,减少了这种问题的发生,但是在外部资源加载的情况下,仍会花费较长时间。要了解如何减少 HTTP 请求以大幅度缩减加载时间,请阅读WordPress
性能。

此外,不少网站管理员在网页中错误的使用 @import 指令 来引入外部样式表。这是一个过时的方法,它会阻止浏览并行下载。link
标签才是最好的选择,它也能提高网站的前端性能。多说一句,通过 link 标签请求加载的外部样式表不会阻止并行下载。

3.减少外部HTTP请求

在很多情况下,网站的大部分加载时间来自于外部的 Http
请求。外部资源的加载速度随着主机提供商的服务器架构、地点等不同而不同。减少外部请求要做的第一步就是简略地检查网站。研究你网站的每个组成部分,消除任何影响访问者体验不好的成分。这些成分可能是:

● 不必要的图片

● 没用的 JavaScript 代码

● 过多的 css

● 多余的插件

在你去掉这些多余的成分之后,再对剩下的内容进行整理,如,压缩工具、CDN 服务和预获取(prefetching)等,这些都是管理 HTTP
请求的最佳选择。除此之外,减少DNS路由查找教程会教你如何一步一步的减少外部 HTTP 请求。

4. 压缩 CSS, JS 和 HTML

压缩技术可以从文件中去掉多余的字符。你在编辑器中写代码的时候,会使用缩进和注释,这些方法无疑会让你的代码简洁而且易读,但它们也会在文档中添加多余的字节。

例如,这是一段压缩之前的代码。

把这段代码压缩后就成了这样。

使用压缩工具可以非常简单地把无用的字节从你的 CSS、JS 和 HTML 文件修剪掉。关于压缩的相关信息,可以参阅如何压缩 CSS、JS 和 HTML。

5. 使用预先获取

预先获取可以在真正需要之前通过取得必需的资源和相关数据来改善访问用户的浏览体验,主要有3类预先获取:

● 链接预先获取

● DNS 预先获取

● 预先渲染

在你离开当前 web 页面之前,使用预先获取方式,对应每个链接的 URL
地址,CSS,图片和脚本都会被预先获取。这保证了访问者能在最短时间内使用链接在画面间切换。

幸运的是,预先获取很容易实现。根据你想要使用的预先获取形式,你只需在网站 HTML 中的链接属性上增加 rel=”prefetch”,rel=”dns-
prefetch”,或者 rel=”prerender” 标记。

6. 使用 CDN 和缓存提高速度

容分发网络能显著提高网站的速度和性能。使用 CDN 时,您可以将网站的静态内容链接到全球各地的服务器扩展网络。如果您的网站观众遍布全球,这项功能十分有用。
CDN 允许您的网站访问者从最近的服务器加载数据。如果您使用 CDN,您网站内的文件将自动压缩,以便在全球范围内快速分发。

CDN 是一种缓存方法,可极大改善资源的分发时间,同时,它还能实现一些其他的缓存技术,如,利用浏览器缓存。

合理地设置浏览器缓存,能让浏览器自动存储某些文件,以便加快传输速度。此方法的配置可以直接在源服务器的配置文件中完成。

了解更多有关缓存和不同类型的缓存方法,请参阅缓存定义。

7. 压缩文件

虽然许多 CDN 服务可以压缩文件,但如果不使用 CDN,您也可以考虑在源服务器上使用文件压缩方法来改进前端优化。
文件压缩能使网站的内容轻量化,更易于管理。 最常用的文件压缩方法之一是 Gzip。 这是缩小文档、音频文件、PNG图像和等其他大文件的绝佳方法。

Brotli 是一个比较新的文件压缩算法,目前正变得越来越受欢迎。 此开放源代码算法由来自 Google
和其他组织的软件工程师定期更新,现已被证明比其他现有压缩方法更好用。 这种算法的支持目前还比较少,但作为后起之秀指日可待。

了解更多信息,请阅读我们有关 Brotli 压缩的完整文章。

对于那些不懂得前端优化的人来说,图片可能会是一个“网站杀手”。大量的写真集和庞大的高清图片会阻塞网页渲染速度。没有优化的高清图片可能会有几兆字节(mb)。因此适当地对它们进行优化可以改善网页的前端性能。

每个图像文件都包含了一些与纯照片或图片无关的信息。比如 JPEG 图片,它包含了日期、地点、相机型号和一些其他不相关的信息。你可以用一些如 Optimus
的优化工具来删除这些多余的图像数据来精简图像的冗长的加载过程。因为 Optimus 是一个无损的图片压缩工具,它不会影响图像画质,只是压缩图片体积。

另外,如果你想进一步的优化一张图片,你可以使用有损压缩,它会删除一些图片里面的数据,因此质量会受损。

一步的学习有损和无损压缩之间的区别,请阅读我们完整的教程。

9. 使用轻量级框架

除非你只用现有的编码知识构建网站,不然,你可以尝试使用一个好的前端框架来避免许多不必要的前端优化错误。虽然有一些更大,更知名的框架能提供更多功能和选项,但它们不一定适合你的
Web 项目。

所以说,不仅确定项目所需功能很重要,选择合适的框架也很重要——它要在提供所需功能的同时保持轻量。最近许多框架都使用简洁的 HTML,CSS 和
JavaScript 代码。

以下是几项可以加快读取的轻量级框架:

● Pure

● Skeleton

● Milligram

框架并不能代替网页设计,编程和维护。举个简单的例子,我们假设框架是一个新房子。房子干净整洁,但它是空的。在你添加家具,家电和装饰品时,你有责任确保房子不会变得凌乱。同样地,当您使用了一个框架,您就有责任确保它不会被冗余的代码,大图片和过多的
HTTP 请求破坏。

前端优化 – 总结

进行前端优化似乎需要花费很大的精力,相信这篇应用指南中的一些小技巧能帮你极大改善网站加载速度。网站加载地越快,则用户体验越佳。因此,
对前端进行优化能使给你和你的用户都带来益处。

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×