APP下载

首屏数据并行式预加载方案的研究与应用∗

2019-06-01闫兴亚潘治颖黄姝琦

计算机与数字工程 2019年5期
关键词:分块服务端页面

闫兴亚 潘治颖 黄姝琦

(西安邮电大学计算机学院 西安 710061)

1 引言

单页应用是近几年来前端技术发展与落地的最典型场景,Angular、Vue、React等前端框架出现的目的都是从架构层面为单页应用提供研发解决方案,提高单页应用的效率。在传统单页应用中,大部分的逻辑都在客户端,服务端提供接口处理数据并提供空的HTML页面,其中服务器端可以使用任意一种语言编写,如 Ruby、Python、Java等[1]。一旦HTML中包含的JavaScript文件被下载,它们将被在客户端执行,从服务器获取数据并直接渲染HTML页面。因此用户将会在加载完整个页面之前看到几秒钟的空页面或者一直加载控件,对此有很多研究表明用户对访问慢站点反应强烈[2]。Amazon claims声称“每提升100ms的页面加载速度将会提升1%的收益”,因此Twitter 40个工程师花费的1年时间去重构,并且经测试,他们实现了服务端渲染整个HTML页面的站点,其首屏页面的呈现时间提高了5倍。

除此之外,由于SEO[3](Search Engine Optimization)是通过客户端向服务器创建请求来解析响应结果的。因此在服务器返回空页面的情况下,无法进行SEO。

由此可见,服务端渲染十分重要。为达到从服务器获取整个HTML并使客户端代码运行快速且更具灵活性的目的,在2011年Nodejitsu便提出了Isomorphic JavaScript[4]的概念。之所以称为 Isomorphic JavaScript,因为从某种意义上讲,无论应用运行在客户端还是服务器端,都具有相同的形式或形态。应用了Isomorphic JavaScript的Web应用,应用和视图层逻辑都可以在前后端运行,应用的性能得以优化并具有更好的维护性,同时可以被SEO。现如今Angular、Vue、React等大量的框架已经应用了Isomorphic JavaScript的概念。

Facebook创始人之一的Dustin Moskovitz,在其应用Luna中尝试使用Isomorphic JavaScript进行构建,这是Isomorphic JavaScript最著名的例子之一。Luna在没有Node.js以前它是构建在v8cgi上,它允许为每一个单独用户会话复制一个完整的应用程序到服务器端运行。它为每个用户创建独立的进程,运行在客户端上的也是服务器端的代码,开启对整个类的高级优化,比如离线支持即时更新。

Mojito[5]是第一个开源的 Isomorphic JavaScript框架,它是完全用Node.js写的框架。Cocktails平台首席架构师Bruno Fernandez-Ruiz称,通过使用Mojito,开发者编写的代码中的95%可以运行在客户端和服务器端,只有5%的代码需要根据客户端做出调整。雅虎希望通过开源Mojito,来创建一个开发者社区并推广该框架。但自从他们在2012年4月开源以来在JavaScript社区没有广泛的流行起来,主要是它依赖于YUI和雅虎这一缺点。

Meteor[6]可能是现今最好的同构项目。Meteor不需要创建低级别的基础设施(如数据同步)或管道来精简和编译代码,而是让开发人员专注于业务功能。它借用几个现有的工具和库,将它们与新的思想以及新的库、标准和服务结合起来,并将它们捆绑在一起,在同一个框架下捆绑并提供所有必需的组件。对于基于分布式应用平台原则的应用,Meteor是很好的选择,但在处理密集计算方面Meteor的性能还有待加强。其次,Meteor对应用的结构和代码没有什么约定,并且仅支持MongoDB数据库,这些都是Meteor有待解决的问题。

除了使用Isomorphic JavaScript,在早些时候,同构库Rendr的出现允许开发人员使用Backbone.js+Handlebars.js构建单页面应用,在服务器端也能全部被渲染。Rendr是为了使Airbnb mobile web有更快的响应速度而创建的产品。对于用户来说高效可用的响应速度是尤为重要的。

但是由于服务端渲染依赖于视图层框架的支持,对于没有使用视图层框架的项目,Isomorphic JavaScript无法支持。因此使用服务端渲染,虽然项目得以优化但丧失了产品的稳定性,代价过大。所以,服务端Isomophic Javascript渲染的应用场景具有局限性[4]。而Rendr力求成为一个库而不是一个框架,所以相比Mojito或Metetor来说,它解决的问题相对较少。

针对没有使用视图层框架项目的优化问题,本文提出首屏呈现节点的处理方式的改造,根据单页应用首屏数据并行式预加载方案,利用浏览器渐进式预加载与http的分块传输编码特性[7],实现应用资源加载、应用初始化、获取首屏数据的并行处理,从而有效地减少首屏页面的呈现时间。

2 首屏呈现节点分析

对于使用包含大量JavaScript的架构的单页应用来说,App Shell是一种常用方法。这种方法依赖渐进式缓存应用外壳让应用运行,并为使用JavaS-cript的每个页面加载动态内容。根据这个架构我们可以看出单页应用首屏呈现节点可以分解为请求入口文件、渲染应用外壳、渲染首屏片段。

本文在此基础上进一步将渲染应用外壳和渲染片段细分为请求入口文件、应用资源加载、应用初始化、获取首屏数据、首屏初始化、组件渲染。

据此首屏呈现耗时的通用计算公式为

请求入口文件+应用资源加载+应用初始化+获取首屏数据+首屏初始化+组件渲染

3 首屏数据渐进式预加载方案

应用资源的加载与应用的初始化并不依赖于首屏数据,因此本文首屏数据并行式预加载的核心思路和优化收益为

1)优化获取首屏数据的速度;

2)预先加载首屏数据,使得多个串行节点并行化。

3.1 应用资源加载与获取首屏数据节点并行

利用分块传输编码可以将请求的报文逐块传输的特性[7],将包含静态资源的标签进行分块传输。浏览器在接收到静态资源标签后会开启http请求线程,在继续解析HTML文档的同时发起对静态资源的请求[8]。服务器在请求首屏数据完成后将首屏数据片段与应用初始化代码分块包含在<script>标签中分块传递给浏览器。由此巧妙地将应用资源加载节点和首屏数据请求节点并行化。当应用初始化完毕后,首屏组件直接读取window.__APP_DATA__数据进行首屏初始化渲染与组件渲染[9]。

具体操作步骤如下:

1)请求首屏数据并在所有数据请求完成后将引用数据资源与应用初始化代码单独分离出来,即将它们包含在首屏数据的内联脚本中,大致如下:

<script>

window.__APP_DATA__=

{/*相关的首屏数据*/};

</script>

<script>{/* 应用初始化代码 */}</script>

2)将入口HTML文件中的静态资源,即静态资源包含在各个资源标签中,如静态的导航栏,加载指示器等,大致如下:

<link

rel=“stylesheet”

href=“/*静态资源对应的地*/”

></link>

3)在服务器端,将入口HTML文件中的静态资源标签与脚本做并行处理,即以分块传输编码的方式将响应分块发送给浏览器[10],在 Node.js[11]中,分块传输编码的实现方式如下:

res.writeHead(200,

{'Transfer-encoding':'chunked'});

res.write();

项目整体架构如图1所示。

图1 项目整体架构图

首屏节点呈现耗时的通用计算公式变为

请求入口文件+Max(应用资源下载,请求首屏数据)+应用初始化+首屏初始化渲染+组件渲染。

此时,首屏各节点耗时如图2所示。

图2 一轮优化后首屏各节点耗时图

3.2 应用初始化,应用资源下载,首屏数据请求节点并行

从上节分析中可知,应用初始化节点耗时很明显,同时该节点要进行必须等待资源文件下载完毕,但理论上可以不依赖首屏数据,所以可以将应用初始化与首屏数据的获取并行处理。

但是如果直接将应用初始化和首屏数据的获取并行化,那么应用初始化会在应用资源文件下载完毕后进行,所以当获取首屏数据时间大于应用资源加载时间与应用初始化时间时,应用会在没有首屏数据的情况下进入首屏渲染节点,从而导致异常。

为了解决这个问题,本文将首屏数据片段的输出变成promise片段。此时应用资源下载完毕后可以无视首屏数据的完成度,直接进入应用初始化节点,首屏渲染在数据promise被resolve后进行即可。通过对数据片段的promise化改造,使得应用初始化节点也加入了并行队列。

首屏呈现耗时的通用计算公式变为

请求入口文件+Max(应用资源加载+应用初始化,请求首屏数据)+首屏初始化渲染 +组件渲染。

此时,首屏各节点耗时如图3所示。

图3 二轮优化后首屏各节点耗时图

4 设计与实现

电子合同系统是专门为销售与商家设计的线上电子合同签署的Web应用。与传统的纸质合同相比,电子合同具有成本低、安全性高、便于监管等优点。该系统基于Node.js开发[12],采用单页面应用的技术架构,实现了前后端分离,代码的可维护性与可读性较高。由于本项目历史悠久不支持视图层框架,所以无法做服务端渲染,用户将会在加载完整个页面之前看到几秒钟的空页面或者一直加载控件,对此有很多研究表明用户对访问慢站点反应强烈。因此,我们采用首屏数据并行加载方案对其进行优化。

1)以分块传输编码[7]的方式将响应报文分块发送给浏览器,在Node.js中,分块传输编码的实现方式如下:

res.writeHead(200,

{'Transfer-encoding':'chunked'});

res.write(`<link

rel=“stylesheet”href=“/* 资源地址 */”>

</link>`);

2)将数据层简单适配下Node端完成数据渐进式预加载。大概如下:

(1)将数据片段的输出变成 promise[13]片段

(2)resolve promise片段,该片段在数据请求成功返回后输出,大概如下:window。__APP_DATA__。resolves.userInfo(

null,data);

(3)reject promise[14]片段,该片段在数据请求失败后输出,大概如下:window。__APP_DATA__。resolves.userInfo(

error);

此时应用资源加载完毕后可以无视首屏数据的完成度,直接进入应用的初始化节点,首屏初始化渲染在数据promise被resolve后渲染即可:

window.__APP_DATA__.appData.then(data=>component.render());

通过对数据片段的promise化改造,使得应用初始化节点也加入了并行队列。

在可用性层面上,整体的系统流畅性不错,但在网速较慢的情况下,首页和部分页面打开极其慢,极大制约了该系统的使用并降低了用户体验水平,这也是绝大多数单页面应用普遍存在的一个问题。

5 实验结果

5.1 实验结果分析

电子合同签约系统优化操作之前,整个首屏呈现timeline如下:

1)首屏呈现时间为185ms(请求入口文件)+500ms(应用资源加载)+950ms(应用初始化)+1050ms(获取首屏数据)+350ms(首屏初始化渲染)+50ms(组件渲染)=3085ms。

2)实现资源文件下载与首屏数据请求节点并行后,最终并行化这块耗时为Max(应用资源加载,获取首屏数据)=1050ms。

根据变化后的节点我们算出首屏呈现时间为:2585ms。

3)应用用初始化,资源文件下载,首屏数据请求节点并行后,最终并行化这块耗时为Max(应用资源加载+应用初始化,获取首屏数据)=1450ms。

根据变化后的节点我们算出首屏呈现时间为2035ms。

经过上述2个步骤改进,应用首屏呈现时间从3085ms->2585ms->2035ms,总体效果约为34%。

在实际项目中耗时是在1935ms左右,比2035ms还要小,主要原因如下:

1)用户在请求入口文件中半个RTT时间,服务器就开始了数据请求。

2)数据请求在服务端进行减少了浏览器与服务端的请求创建开销,同时数据请求在内网进行,总体调用速度也会加快。

当首屏数据请求数超过浏览器并发请求数时,该方案收益会更明显,因为Node端没有并发限制,甚至在Node端与后端服务的交互中可以采用更高效的协议如HTTP2来提高调用速度。

5.2 优化小结

我们在单页应用的性能优化上基于很朴素的并行化理念实施了首屏数据渐进式预加载方案,在实际项目中也得到了较为明显的效果,减少了1050ms的加载时间,整体的节点变化如下。

优化前首屏各节点耗时如图4所示。

图4 优化前首屏各节点耗时图

优化后首屏各节点耗时如图5所示。

图5 优化后首屏各节点耗时图

最终数据渐进式预加载方案的首屏呈现时间计算公式为

请求入口文件+Max(应用资源加载+应用初始化,获取首屏数据)+首屏初始化+组件渲染。

6 结语

单页应用作为在用户体验方面能够与桌面程序媲美的Web应用,其应用场景越来越广泛。一个单页应用是否成功,很大程度上取决于其用户体验的好坏,提升用户体验的一个关键因素便是缩短首屏页面呈现时间。

本文所提出的首屏数据并行式预加载方案能够能有效减少首屏呈现时间,并且具有可操作性强、实现成本低的优点。一方面,对客户端代码来说本方案基本可以做到透明化,在实际的开发过程中采用基于AOP拦截方案,通过配置化的方式让客户端的代码改造仅局限在配置文件,应用代码基本未改动。另一方面,分层合理的应用只需要将数据层简单适配下Node端即可完成数据渐进式预加载,这对底层基础框架在视图层没有支持同构的应用来说,整个改造成本可以说大大减小,且收益明显。

猜你喜欢

分块服务端页面
面向量化分块压缩感知的区域层次化预测编码
刷新生活的页面
钢结构工程分块滑移安装施工方法探讨
答案
让Word同时拥有横向页和纵向页
一种面向不等尺寸分块海量数据集的并行体绘制算法
分块矩阵初等变换的妙用
多人联机对战游戏的设计与实现
基于三层结构下机房管理系统的实现分析
基于三层结构下机房管理系统的实现分析