优化无止境,爱奇艺中后台 Web 应用性能优化实践

标签: tuicool | 发表时间:2021-01-11 00:00 | 作者:
出处:http://itindex.net/relian

爱奇艺视频生产智能云平台系统在今年进行了一次 重大升级,前端团队也趁此机会将 底层技术架构从三年前的 Arm.js(内部MVC框架)+ Java BFF + Velocity 模板完全切换到了 Vue.js + Node.js BFF 的技术栈。

新的前端应是一个拥有超过 十个业务模块的单页面应用,每个模块已经通过路由懒加载进行了拆分,同时公共的第三方依赖也拆分到了单独的 Vendor 文件。不过在上线试用初期,用户还是普遍反馈页面打开速度较老版本有比较明显的下降,存在几秒钟不等的白屏等待时间。

为了提升用户体验和使用效率,团队内部对新版前端应用进行了多次优化,最终效果 提升非常显著。本文的主要内容就是针对中后台 Web 应用性能的 分析思路解决方案的总结分享。

问题梳理

我们先通过提问题的方式,从 资源文件加载、页面渲染性能、接口响应速度等三个方面分别列出了一些可能存在性能瓶颈的环节。

资源加载问题

在一个复杂的 Web 应用中,通常会依赖很多 JS/CSS/Images 等资源文件。如何在最短时间内获取页面所需的最小资源,我们需要考虑以下几个问题:

  • 源码中有无 冗余的模块?是否进行了 压缩、合并等操作?

  • 服务器响应及网络传输速度是否正常?有没有最大化利用浏览器的并发请求?

  • 资源文件的缓存策略是否合理?是否每次发布上线都需要重新请求所有文件?

  • 首次页面渲染是否下载了不必要的资源文件?每次渲染所需的资源文件能不能提前加载?

页面渲染问题

由于 JS 是在单线程中执行,而 Vue.js 框架的大部分渲染任务都在浏览器端完成。为了解决白屏、卡顿等问题,我们需要考虑以下几个问题:

  • 是否可以通过骨架屏等方式提前渲染核心布局?

  • 主线程是否存在非常耗时的长任务?是否可以进行任务分片、延迟渲染?

  • 是否存在时间复杂度过高的算法?是否存在大量重复计算?

  • 是否重复初始化相同的对象?是否存在内存泄露?

接口速度问题

在列表查询等依赖后台数据展示的页面,接口的响应速度也至关重要。由于我们通过 Node.js 搭建的 BFF 来整合多个服务提供方的接口,因此可能存在以下几个问题:

  • 后端服务提供的接口速度是否响应慢?网关、数据库、索引等服务是否正常?

  • 针对实时性要求较低的数据,是否可以利用缓存服务?

  • 同时调用多方接口时,是否最大化进行并发请求?非必要接口是否可以单独发起请求?

  • 与浏览器脚本一样,是否存在复杂算法、内存泄露等问题代码?

解决方案

带着以上的这些问题,我们开始着手对现有的应用进行一次详细的检查,逐步定位影响性能的关键问题并一一进行解决。

资源加载优化

Webpack 构建问题分析

由于我们的项目通过 Webpack 4.x 构建,因此为了分析资源文件的个数及大小,采用了 Webpack 插件webpack-bundle-analyzer对产出的静态资源文件进行了统计,如下图所示(截取了几个体积较大的文件)。

根据统计我们发现了以下几个主要问题:

  • 缓存问题。每次改动任意代码,所有生成的 JS/CSS 等文件的 Hash 值都发生了变化,这意味着每次发布上线,浏览器都需要重新请求全部资源。

  • 文件大小。通过 node_modules 生成的 chunk-vendor 原始大小超过 1.5 M。其中,体积最大的是 ElementUI,超过 650K,其次是 moment.js,体积超过 250K。剩余部分则由 Vue.js、Lodash 等基础类库组成。

  • 重复打包。部分业务模块对应的 chunk 文件原始大小在 500K 左右。其原因是使用到了 d3,echarts 等依赖的模块,直接将它们打包到了对应模块中。而这些第三方库,占整个文件大小的 70% 左右。

  • 资源个数。由 Webpack 自动生成了多个模块间的公共 chunk,大小在几 K 到一百多 K 不等。例如有三个模块 a,b,c,则自动生成的 chunk 包含多种不同的组合 a~b.js,a~c.js,a~b~c.js,请求 a 模块的时候也会同步加载这几个文件。随着模块数量增加,组合也更复杂,无形中也增加了请求的数量。

浏览器加载速度分析

通过浏览器 Network 工具,我们发现服务器缓存、网络传输等对加载速度影响很小,导致慢的几个主要问题如下:

  • 并发数量。通过构建得到的静态资源文件都部署到一个静态域名下面,导致需要排队下载文件。

  • 顺序问题。一些非首次渲染所需要的 JS 文件(如播放器 SDK 、流程图 SDK 等)在页面打开的时候就进行了阻塞加载。

资源构建及部署优化方案

针对以上问题,我们对 Webpack 配置方式做了以下几点改进。

  • 单独部署基础库至 CDN。生产环境将 Vue.js + VueRouter + Vuex + VueCompositionAPI + ElementUI + Lodash 等基础类库通过 webpack.DllPlugin 提前构建为 library.dll.js 并单独部署,同时整个站点中通过 prefetch 提前加载。

  • 单独部署样式主题至 CDN。项目中用到的 ElementUI 组件样式及团队内部开发的 MaterialTheme 主题样式放弃从 NPM 引入 Sass 源码。而是提前构建好 9 种不同颜色的主题,提前部署至 CDN,并通过 prefetch 提前加载。项目中的自定义样式则通过 Sass Mixin 生成不同主题的规则。

<link    
href="//static.iqiyi.com/lego/theme/element-ui/1.0.0/css/cyan.css"
rel="prefetch"
/>
<link
href="//static.iqiyi.com/lego/theme/element-material/2.0.0/css/cyan.css"
rel="prefetch"
/>
  • 将业务代码部署至与基础库不同的域名。提升浏览器并发请求的数量。

  • 将播放器 SDK、流程图 SDK 等非首次渲染必须的 JS 文件通过 defer 等方式进行异步加载,或改为组件初始化时动态请求。

  • 删除 moment.js 等非必须的第三方类库。通过查看项目源码,发现仅几个地方用到了 moment.js 的格式化功能,因此我们选择通过自己实现一个仅几十行的工具函数来替换。此外根据项目实际情况,也可以考虑在项目中引入体积更小的类库,例如 Day.js 等。

  • 优化 Webpack 的 splitChunks 策略。将 d3,echarts 等依赖抽取为单独的 chunk。此外,考虑到不同模块之间自动生成的公共 chunk(类似 a~b~c.js)文件不大,反而增加了请求数量,因此禁用了该项配置。同时,显示地将各模块间公共的部分(项目中统一放在 src/common 目录下)打包至 chunk-common 文件中。

// webpack config    
{
optimization: {
splitChunks: {
cacheGroups: {
// 禁用默认拆分的 chunk
default: false,
// 显示抽取项目公共 chunk
common: {
name:'chunk-common',
test: /src[\\/]common/,
chunks:'all'
},
// 抽取 d3/echarts 等第三方类库
d3: {
name:'chunk-d3',
test: /[\\/]node_modules[\\/](d3|dagre|graphlib)/,
priority:100,
chunks:'all'
},
echarts: {
name:'chunk-echarts',
test: /[\\/]node_modules[\\/](echarts|zrender)/,
priority:110,
chunks:'all'
}
}
}
},
}
  • 优化构建后文件名中的Hash。在生产环境改用 contenthash 来命名文件,仅当包含的文件内容发生改变时才会重新生成新的文件名,最大化利用缓存。

// webpack config
{
output: {
filename: 'js/[name].[contenthash].js',
chunkFilename: 'js/[name].[contenthash].js'
}
}

经过以上优化,最终构建的 chunk-vendor 大小在 500K 左右,体积大约减小 2/3;新抽取的项目公共文件 chunk-common 大小 300K 左右;各个模块打包的文件大小则在 200K 左右, 体积大约减小 3/5。同时,结合 CDN 部署基础类库,prefetch 预加载及 contenthash 缓存控制等,资源加载的速度大幅度提升。

页面渲染优化

考虑到业务场景及开发成本,新版本的前端应用并没有实现服务器端渲染,存在着较长的白屏时间。而老版本则通过 Java + Velocity 在服务器端完成渲染,两相对比,用户体验相差甚多。

浏览器渲染性能分析

为了解决这个问题,我们通过 Chrome Performance 对页面的渲染性能进行了完整的分析。

由于生产环境代码已经压缩,这里建议在开发环境录制 Profile,可以直接定位到相关源码。录制后的时间线展示参见下面截图。

其中我们需要重点关注的几个维度如下:

  • Frames:渲染的 FPS 以及不同时间点的渲染结果。

  • Main:渲染主线程,包括 HTML 解析,JavaScript 执行等任务。

  • Timings:包括 FP、DCL、FCP、LCP 等指标,以及通过 Performance API 记录的运行时间。Vue.js 2.x 中可以通过 Vue.config.performance = true; 开启组件性能记录。下图的截图展示了 Vue.js 组件的渲染耗时情况。

经过分析,我们发现以下几个主要问题:

  • 路由激活后的首次渲染任务耗时特别长,已经超过了 2 秒。其中,站点导航、侧边栏等就占用了一半以上的时间。

  • 导航组件中,用于判断链接权限的 AuthService.hasURIAuth 方法占用了 80% 的时间。

  • 在通过配置渲染的动态表单页中,核心组件 FormBuilder 渲染时间也在 2 秒左右。

页面渲染整体优化方案

针对以上问题,我们进行了以下几点改进:

  • 通过服务器端渲染骨架屏,包括导航等页面基础布局。从视觉效果上减少用户的心理等待。

  • 减少首屏渲染的组件数量。将初始为隐藏状态的导航二级菜单、站点侧边栏、列表高级搜索弹窗等组件通过 webpack 提取至异步 chunk 中,在用户交互时再异步渲染。

// AppLayout.vue    
{
components: {
AppDrawer:()=>
import(
/* webpackChunkName: 'chunk-async-common' */
'./AppDrawer'
),
AppHeader
},
}
  • 优化耗时的 JavaScript 函数。这一步需要结合实际代码实现进行优化,以上面提到的权限判断方法 AuthService.hasURIAuth 为例,其中最突出的问题就是循环内函数重复执行以及正则表达式重复创建。我们通过 Memoization 的方式为耗时函数添加记忆化功能,当参数相同时直接返回记忆值;通过 Cache 将正则表达式实例缓存起来以便重复使用。

  • 将根据配置进行渲染的动态表单 FormBuilder 手动拆分为多个渲染任务。由于业务场景的复杂性,通常一个表单拥有 80 余个字段。而在 Vue.js 里面,一次数据变化触发的渲染任务是无法直接拆分的。这里我们采取了另一种方式,将表单配置拆分为多段,首次渲染时仅传递第一段配置,然后在后续的渲染周期依次将配置拼接上去。

<template>    
<form-builder:config="formConfig"></form-builder>
</template>
{    
created() {
this.getFormConfig().then(()=>{
this.startWork();
});
},
methods: {
startWork() {
constwork =()=>{
// 任务调度器
returnscheduler.next(()=>{
// 逐步拼接表单配置
this.formConfig =this.concatNextFormConfig();

if(!scheduler.done()) {
// 循环执行任务
work();
}
});
};

// 启动首次任务
work();
}
}
}

接口速度优化

BFF 性能分析

由于业务流程复杂,前端会调用多个服务接口,并对数据进行二次处理,因此一直由前端来负责Java Web层(BFF)的开发。本次升级为了开发更简便,引入了基于TypeScript的NestJS框架替换原来Spring MVC,由NestJS封装面向前端的接口给 VueJS应用。为了定位其中潜在的性能问题,我们做了一些通用的扩展:

  • 为所有封装的接口添加自定义中间件 TimeMiddleware,用于统计接口的整体响应速度。

  • 为 axios 统一添加 interceptor,用于统计 BFF 调用第三方接口的响应速度。

最后,通过日志、Apache JMeter 等工具对核心接口进行分析,我们主要发现以下几个问题:

  • 在对千万量级的索引数据进行分页查询的接口 A 中,当前 ES 的查询速度不理想,平均耗时在 2.6 秒左右。

  • 在同时调用多方服务的接口 B 中,存在不必要的串行。此外,其中一个标签查询服务平均耗时在 700ms 左右,成为影响速度的关键因素。

  • 在获取用户信息的接口 C 中,有 20% 左右的请求耗时在 600ms 左右,而其他的请求仅耗时 50ms。经过定位发现是服务集群中某台服务器跨地区导致。

  • 大部分接口都依赖了一个获取频道列表的基础服务,实时性要求很低,然而每次都是通过接口实时获取,耗时大约 50 ms。

  • 整个应用的日志服务继承了 NestJS 的 logger.service ,它默认是通过 process.stdout 同步输出的。因此日志内容较多时在部分机器上开销也很大,平均耗时 100ms 左右。

BFF 整体优化方案

针对以上问题,我们进行了以下几点改进:

  • 后端同学优化 ES 查询服务,新增多台物理机进行扩容。优化后平均耗时小于 1 秒,速度提升超过 60%。

  • 后端同学为标签查询服务添加缓存机制,优化后平均耗时 200ms 左右,整体提升超过 70%。

  • 移除集群中的跨地区服务器,保证各服务之间尽量在同一个地区、机房。

  • 大化地并行请求,减少请求耗时的关键路径。以其中一个接口为例,优化前平均耗时 1.3 秒,优化后平均耗时仅 700ms,提升 45% 左右。

  • 实时性要求较低的服务通过 Redis 缓存查询结果,例如频道查询服务,平均耗时从 50ms 减少至 15ms,提升 70% 左右。

  • 生产环境的日志取消输出到 process.stdout,通过 winston 等日志框架将其异步写入至指定文件中。

优化后整体效果展示

资源加载速度展示

通过减少文件大小及个数、缓存、并发、预加载、懒加载等各种优化,获取核心资源整体耗时控制在 200ms 左右。

首次加载主题样式与切换主题示例(Prefetch)

异步加载路由及组件示例(Prefetch)

页面渲染速度展示

通过异步渲染隐藏组件、优化耗时函数、任务分片、骨架屏等方式,让用户尽早看到内容的同时,将首次路由渲染的时间控制在 1 秒以内,结合浏览器自身的优化,在电脑网速及性能正常的情况下,已经感知不到白屏的存在。

接口相应速度展示

通过扩容、缓存、并发、优化耗时函数等方式,我们将核心的几个查询接口的速度也控制在了 1 秒左右。

优化前后核心数据对比

优化环节

优化前

优化后

首次下载脚本资源

实际下载 JS 文件 7 个,整体大小 3.5M

实际下载 JS 文件 4 个,整体大小 1.8M

路由首次渲染时间

平均 2.66 秒

平均 790ms

索引分页查询响应时间

平均 2.60 秒

平均 1.03 秒

后记

前端的性能优化涉及到方方面面,每一个环节其实都有优化的空间。这次实践,我们针对项目的实际场景,主要从资源加载、渲染性能和接口速度三个方面来分析并解决问题,一步一步提升页面的打开速度,也为用户带来了更好的使用体验。当然,优化无止境,希望本文能起到抛砖引玉的作用,感兴趣的同学可以留言讨论。

相关 [优化 爱奇艺 后台] 推荐:

优化无止境,爱奇艺中后台 Web 应用性能优化实践

- - IT瘾-tuicool
爱奇艺视频生产智能云平台系统在今年进行了一次 重大升级,前端团队也趁此机会将 底层技术架构从三年前的 Arm.js(内部MVC框架)+ Java BFF + Velocity 模板完全切换到了 Vue.js + Node.js BFF 的技术栈. 新的前端应是一个拥有超过 十个业务模块的单页面应用,每个模块已经通过路由懒加载进行了拆分,同时公共的第三方依赖也拆分到了单独的 Vendor 文件.

爱奇艺视频后台从“单兵作战”到“团队协作”的微服务实践

- - DockOne.io
系统越做越大,功能越加越多,我们是否有如下经历:. 一次小的需求,评估由此产生的影响成本超过开发需求本身. 系统几经交接或升级,接口文档丢失或跟代码严重不符. 每天疲于排查线上问题和修复线上数据,没有精力代码优化. 由于创建/开发/部署新服务的成本,不断的将无关的功能添加到臃肿的服务. 线上服务一个功能或者中间件的中断,导致整个系统不能提供服务.

Optimize DB:在 WordPress 后台优化 MySQL 数据库

- - 我爱水煮鱼
随着 MySQL 的使用,包括 BLOB 和 VARCHAR 字节的表将变得比较繁冗,因为这些字段长度不同,对记录进行插入、更新或删除时,会占有不同大小的空间,记录就会变成碎片,且留下空闲的空间. 就像具有碎片的磁盘,会降低性能,需要整理,因此要优化. 如何优化 WordPress 数据库. 而 WordPress 使用的数据库正是 MySQL,所以当你觉得 WordPress 速度比较慢的时候,对 MySQL 进行优化下,可以相当程度上提高 WordPress 的速度.

2015年爱奇艺营收52.9亿 运营亏损23.8亿

- - 199IT互联网数据中心
中国网络视频领域正发生微妙变化. 就在上周,合一集团(优酷土豆)宣布与阿里巴巴集团已完成合并交易,正式成阿里巴巴旗下全资子公司,优酷土豆已经正式从美股市场退市. 优酷土豆CEO古永锵表示,公司选择私有化目的有三个,回归国内资本市场,与阿里联动、超越纽约,现在优酷土豆已启动国内上市计划,目标是3年之内在国内资本市场上市.

爱奇艺短视频打标签技术解析

- - IT瘾-dev
写在前面 最近几年出现了很多以短视频的创作和分发作为主打的手机应用软件,这极大地丰富了文本和图像之外的信息创作和分发方式. 这些短视频应用自从问世以后,便迅速地占领了市场,得到了广大用户的青睐. 目前,短视频正逐渐成为互联网上的一种重要的信息传播方式,由此产生了大量的短视频数据. 为了更好地利用短视频数据,提升短视频的创作和分发效果及效率,需要为短视频打上各种有用的标签,这些标签可以作为短视频所记录的内容的概括和总结.

爱奇艺短视频分类技术解析

- - 机器之心
近年来,短视频领域一直广受关注,且发展迅速. 每天有大量UGC短视频被生产、分发和消费,为生产系统带来了巨大的压力,其中的难点之一就是为每个短视频快速、准确地打上标签. 为了解决人工编辑的时效和积压问题,自动化标签技术成为各大内容领域公司都非常关注的关键课题. 短视频大规模层次分类作为内容理解技术的一个重要方向,为爱奇艺的短视频智能分发业务提供着强力支持,其输出被称为“类型标签”.

爱奇艺微服务监控的探索与实践

- - DockOne.io
作为一线程序猿,是否有过类似经历. 新接手一个系统,各接口入口流量是多少,又是哪些业务方在调用. 系统大量异常报警,如何快速锁定影响范围,恢复故障并定位问题. 监控的重要性不言而喻,可是接入监控的额外工作又让人望而却步. 每天编写代码之余,又要花多少时间定位线上问题. 自己负责的系统故障,是否要等调用方反馈才知道.

Netflix原创这么厉害 爱奇艺优酷为什么没有追?

- - 今日话题 - 雪球
Netflix 今年计划推出700部影视作品,原创内容风生水起,今年以来涨幅也已超过80%,这么看,自制这条路已经走通了,那么爱奇艺优酷为什么不放手大干呢. 思考一个问题,影视原创,中国的视频网站有没有机会. 这虽然是个老话题了,之所以翻出来,是因为过去市场对互联网公司自制内容还抱有极大的疑问,而现在Netflix已经证明了这条路的可行性.

爱奇艺、虎牙、B站、映客的不同之处 - 老虎社区

- -
$虎牙直播(HUYA)$登录纽交所,IPO首日高开30%,最终收涨34%,热度瞬间冲到中概榜首.   有人总结虎牙上市是占尽天时地利人和,想想不无道理. 天时:上市时间点选得好,踏入5月以来,大盘强劲、相关的中概新股爱奇艺和B站也都大幅反弹,过去一周,. $爱奇艺(IQ)$一周大涨27%,. $B站(BILI)$过去一周累计大涨15%;.

爱奇艺个性化推荐排序实践 | 人人都是产品经理

- -
在海量的内容在满足了我们需求的同时,也使我们寻找所需内容更加困难,在这种情况下个性化推荐应运而生. 在当前这个移动互联网时代,除了专业内容的丰富,UGC内容更是爆发式发展,每个用户既是内容的消费者,也成为了内容的创造者. 这些海量的内容在满足了我们需求的同时,也使我们寻找所需内容更加困难,在这种情况下个性化推荐应运而生.