【ArchSummit架构师峰会】探讨数据与人工智能相互驱动的关系>>> 了解详情
写点什么

百度 SSP 单页式应用性能优化实践

  • 2017-03-30
  • 本文字数:3649 字

    阅读完需:约 12 分钟

针对首页和部分页面打开速度慢的问题,百度 SSP 前端团队对单页式应用性能进行了优化。本文将介绍其中一个性能优化方案: 基于 HTTP Chunk 的首屏数据渐进式预加载方案,该方案减少了 1.2s 的加载时间。同时对比其与同构渲染方案的异同。

背景介绍

单页式应用是近几年来前端技术栈发展与落地的最典型场景,Angular、Vue、React 等,这些相关的技术栈目的都是从架构层面为单页式应用提供研发解决方案,着重解决单页式应用的研发效率。基础框架的进化也催生着关联工具链路的发展,如 Yeoman,Grunt -> Gulp -> 各种 cli,Webpack1/2, Babel 等。

随着研发链路体系的稳定成熟,在功能上能够及时满足用户后,百度展示广告部流量端前端团队(百度 SSP 前端团队)开始将精力集中关注产品的可用性层面。经过和产品,运营,用研等多个团队配合,我们走访了多位使用我们产品的用户,产出了一份流量端产品可用性得报告。除了部分交互和产品流程设计上的问题,另一个主要问题就是用户反馈整体的系统流畅性不错,但首页和部分页面打开极其慢,针对这块问题,我们开始了对单页式应用性能优化的探索和实践

本文接下来将一步一步阐述对应用首屏呈现中各个节点的拆解,并根据拆解的节点推导出我的优化思路,最终为大家介绍我提出并尝试的第一个性能优化方案: 首屏数据渐进式预加载

首屏呈现节点分析

在进行任何的性能优化之前,我们都应该先找出系统的性能瓶颈点,从而找出最有价值的优化方向。

绝大多数的单页式应用都符合 Application Shell 架构,根据这个架构我们可以看出一个应用首屏呈现节点可以分解为:请求入口页 -> 渲染应用外壳 -> 渲染首屏片段。我在此基础上进一步将三个节点细分如下:

(点击放大图像)

即对渲染应用外壳和渲染片段这块细分为:应用资源加载,应用初始化,片段资源加载,片段初始化,片段数据加载,片段渲染这些节点。

有了这些细分节点,再将埋点记录的真实用户数据代入:

(点击放大图像)

得出我们的首屏时间为:

** T(s) = T1 + … + T7 = 2800ms **

注:我们一般都将首屏资源一起与应用资源打包在一起,因此这里耗时认为是 0。

整个 timeline 如下:

(点击放大图像)

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

根据上面的节点数据,首屏数据渐进式预加载的优化思路也得到了体现:

  1. 优化首屏数据加载节点的速度。
  2. 预先加载首屏数据,使得多个串行节点并行化。

接下来详细介绍我们的优化步骤。第 1 点会在第一步优化中体现,但核心思路和主要优化收益更多体现在第 2 点:多个串行节点并行化。

Step1:资源文件下载与首屏数据请求节点并行

为了达到资源下载与数据请求并行的效果,我们充分利用了HTTP Chunk 传输与浏览器的渐进式渲染特性

  1. 将入口页分为静态片段和数据片段:静态片段包含了各个资源标签(script,link),静态的导航栏,加载指示器等;数据片段则是包含首屏数据的内联脚本,大至如下:
复制代码
<script>window.__APP_DATA__ = { /* 相关的首屏数据 */ };</script>
  1. 浏览器请求入口页时,入口页服务器 (这里我们用了 NodeJS ) 并行 做以下操作:
  • HTTP Chunk 方式输出静态片段
  • 请求首屏数据并在所有数据请求完成后将数据片段和应用初始化代码返回给浏览器。

注:http chunk 方式输出在 NodeJS 中及其容易满足,简单的 res.write(chunk) 即可。

整体架构如下:

(点击放大图像)

浏览器的渐进式渲染特性在收到静态片段并解析后立刻去下载资源,由此巧妙的将应用资源加载节点和首屏数据请求节点并行化;当应用初始化完毕后,首屏组件直接读取 window.__APP_DATA__拿到数据渲染即可。

整个首屏呈现 timeline 变化如下:

(点击放大图像)

最终并行化这块耗时为:Max(下载资源文件,请求首屏数据输出片段) = 1000ms。

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

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

下载静态片段 + Max(下载资源文件,请求首屏数据) + 应用初始化 + 首屏初始化 + 首屏渲染

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

在 Step1 的基础上继续分析,应用初始化节点耗时也很明显,同时该节点要进行必须等待资源文件下载完毕,但理论上可以不依赖我们的首屏数据,还是可以让其和首屏数据请求并行。

这里我们无法在 Step1 方案上直接将应用初始化和数据请求并行化,主要原因在于当首屏数据请求时间大于资源加载 + 应用初始化完成时间时,应用会在没有数据的情况下进入收入首屏渲染节点,从而导致异常。

解决方案是将数据片段的输出变成 promise 片段:

  1. pending promise 片段,与静态片段一起输出,大概如下:
复制代码
<script>
window.__APP_DATA__ = {
RESOLVERS: {}
userInfo: new Promise((resolve, reject) => {
// 超时认为失败
let timer = setTimeout(reject.bind(null, {message: 'timeout'}), 12000);
window.__APP_DATA__.userInfo = (err, data) => {
clearTimeout(timer);
err ? reject(err) : resolve(data)
}
})
};
</script>
  1. resolve promise 片段,该片段在数据请求成功返回后输出,大概如下:
复制代码
<script>window.__APP_DATA__.RESOLVERS.userInfo(null, data); </script>
  1. reject promise 片段,该片段在数据请求失败后输出,大概如下:
复制代码
<script>window.__APP_DATA__.RESOLVERS.userInfo(error); </script>

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

复制代码
window.__APP_DATA__.userInfo.then(data => component.render());

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

整个首屏呈现 timeline 变化如下:

(点击放大图像)

根据变化后的节点我们得到首屏呈现时间为: 1800ms

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

下载静态片段 + Max(下载资源文件 + 应用初始化,请求首屏数据) + 首屏初始化 + 首屏渲染

优化小结

经过上述 2 个步骤改进,我们应用首屏呈现时间从 2800ms -> 2350ms -> 1800ms,总体效果约为 36%,可以看到是收益还是很可观的。

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

  1. 用户在请求入口页中半个 RTT 时间,服务器就开始了数据请求。
  2. 数据请求在服务端进行减少了浏览器与服务端的请求创建开销,同时数据请求在内网进行,总体调用速度也会加快。

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

与 SSR 方案的对比

看到这里,相信很多人会问,为啥不用服务端渲染直出 HTML 呢,或者和服务端渲染方案相比有何优势?

事实上,一开始我和大多数人想到的优化方案就是服务端渲染,但真正的障碍在于服务端渲染依赖视图层框架的支持,而我们的项目历史悠久,视图层框架并不支持这一点,为了优化而丧失产品的稳定性得不偿失。

当然,在另辟蹊径使用了数据渐进式预加载方案后,我总结该方案与 SSR 的对比如下。

优势

  1. 对客户端代码来说数据渐进式预加载方案实现成本非常简单,基本可以做到透明化,我们在实际的开发过程中采用基于 uIoC( https://github.com/ecomfe/uioc ) 提供的 AOP 拦截方案,通过配置化的方式让客户端的代码改造仅局限在配置文件,应用代码基本未改动。
  2. 对 NodeJS 端来说,分层合理的应用只需要将数据层简单适配下 NodeJS 端即可完成数据渐进式预加载,这对底层基础框架在视图层没有支持同构的应用来说,整个改造成本可以说大大减小,且收益明显。我们目前的应用基于自有的一套 MVC 框架,仅仅是将 Model 层简单适配 NodeJS 端执行输出数据。
  3. 服务端渲染方案如果未能提供较基于 BigPipe 的渲染,总体的页面呈现速度还是不如数据渐进式预加载的,且目前我也暂时还没有在三大框架中发现有一套基于 BigPipe 的服务端渲染方案。

不足

整体呈现速度可能不如结合了 BigPipe 的服务端渲染方案,但这点没有经过论证,毕竟数据渐进式预加载与服务端同构渲染的区别仅仅在于渲染环节放在客户端还是服务端:渲染看的是 CPU,服务端的 CPU 资源是有限的,要服务诸多请求,而客户端渲染则基本无此压力,渲染能力未必弱于服务端。

总结

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

优化前:

(点击放大图像)

优化后:

(点击放大图像)

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

下载静态片段 + Max(应用资源加载 + 应用初始化,请求首屏数据) + 首屏初始化 + 首屏渲染

这里忽略了影响很小的片段传输时间,有打算尝试的朋友可以将自己应用的相关节点数据代入计算即可。

数据渐进式预加载,服务端同构渲染,客户端渲染三种方案各有优缺和场景,个人未来计划是将三种方案结合实时流量数据动态切换:在服务器压力不大时用同构渲染;服务器压力较大时用数据预加载;服务器压力很大时用客户端渲染。


感谢韩婷对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们。

2017-03-30 17:342437

评论

发布
暂无评论
发现更多内容

iOS 15隐私升级及注意事项

阿里巴巴大淘宝技术

ios WWDC21

新能源车的发展趋势

石云升

学习 新能源汽车 7月日更

利用 IComparable<T> 以及 IComparer<T> 定义顺序关系

喵叔

7月日更

免费分享入门学习Java的优秀图书

Java入门到架构

Java 书籍 Java入门

将 FeignClient 的请求记录成 cURL 格式

哈德韦

微服务 前后端分离 log4j curl Spring boot Feign

2022秋春招/提前批面经分享总结(字节、腾讯、阿里)

学无止境的阿奔

腾讯 面试 阿里 字节 校招

AQS介绍和原理分析(下)

追风少年

并发编程 AQS

08 | 指针系列(二):记住,指针变量也是变量(2)

Nydia

Vitalik Buterin眼中的区块链信任模型

CECBC

字节跳动实习生删库高操作

学神来啦

Linux 程序员 职场搞笑 linux运维

如何实现跨设备的双向连接? Labo涂鸦鸿蒙亲子版分布式开发技术分享

科技汇

从特斯拉召回事件,窥探OTA汽车进化真面目

脑极体

为什么AI需要大大大大大模型?

白洞计划

大专的我,闭关苦学56天,含泪拿下阿里offer,五轮面试,六个小时灵魂拷问

Java架构师迁哥

在线HTML转JS/JSON工具

入门小站

工具

为技术系统打“疫苗”,爱奇艺攻防演练平台的探索实践

爱奇艺技术产品团队

架构 安全攻防 开发 混沌工程

模块1

cherrycheek

Flink知识点总结

五分钟学大数据

flink 7月日更

Linux常用命令-文件操作

正向成长

linux命令

全表遍历并处理数据有点慢?放开!我来!

林一

jpa Stram @QueryHints

CVPR2021竞赛结果出炉,阿里淘系多媒体算法包揽3项国际冠军

阿里巴巴大淘宝技术

CVPR

一文看懂 Context

Rayjun

Go 语言

什么是数据仓库

奔向架构师

数据库 数据仓库 数据架构 话题讨论 7月日更

花2个月备战字节,3轮面试拿下总包60W Offer!

Java架构师迁哥

云原生负载均衡和网关应用实践

火山引擎开发者社区

云原生 后端

网络攻防学习笔记 Day62

穿过生命散发芬芳

网络攻防 7月日更

为什么AI需要大大大大大模型?

脑极体

科创人|容联七陌CEO陈光:90后街舞少年的CEO修炼手册

科创人

人工智能 创业 程序员

国内市场主流音视频产品主要功能分析

anyRTC开发者

音视频 WebRTC 语音通话 实时通讯 视频通话

Python协程 & 异步编程(asyncio) 入门介绍

行者AI

#python

Linux之which命令

入门小站

Linux

百度SSP单页式应用性能优化实践_最佳实践_邓欣欣_InfoQ精选文章