性能调优梳理

830 阅读16分钟

性能优化点:

  1. 视觉稳定性指标 CLS(布局偏移量) 可以依赖谷歌lighthouse做本地采集

  2. 白屏时间 输入url 到渲染出第一个字符的时间);

  3. 首屏时间 白屏时间+ 渲染时间;

  4. 交互性优化: FID(首次输入延迟) PSI(视觉变化率)

  5. 基于业务的性能优化

白屏和首屏时间是用户体验很大的一个点, 也是我们性能整治的重点项目

白屏及优化

  1. 输入url

  2. 浏览器向DNS服务器发起DNS查询, 获取IP

  3. 浏览器通过IP地址找到目标服务器, 发起TCP三次握手及TLS协商, 建立TCP链接

  4. 建立链接后, 发起HTTP请求,

  5. 服务端接收后对请求进行响应

  6. 浏览器从响应结果中拿到数据, 并进行解析和渲染

大概分为以下3个阶段:

客户端发起请求阶段:

  1. 本地缓存 (强缓存, 协商缓存)

  2. DNS查询 (DNS预获取)

  3. HTTP请求 (域名规划)

服务端数据处理请求阶段:

webserver 接收到请求后, 从数据存储层取到数据, 再返回给前端的过程;

  1. 服务端接收到http请求后, 会做一些请求参数处理及权限校验.

  2. 讲请求参数发送到数据存储服务,

  3. 服务端程序会从数据存储中取到数据, 进行数据加工聚合处理,

  4. 通过jsonp或者ajax发送给前端

瓶颈点:

  1. 数据缓存处理 ( service worker 数据接口缓存, 本地存储的接口缓存, CDN)

  2. Gzip压缩

  3. 是否进行了重定向 (服务端302重定向, meta标签实现的重定向, 及js跳转, 都会重新触发DNS查询及后面步骤)

页面解析和渲染阶段的瓶颈点:

dom树瓶颈
  1. (不满足语义化时, 浏览器会进行预发纠错)

  2. DOM节点数量越多, 构建DOM树时间越长

  3. 包含

cssom树:

  1. 避免深层嵌套

  2. 避免通配符 及标签选择器

布局的瓶颈点

避免回流和重绘

弱网性能优化

1. 合并请求

2. 小图标采用base64 Encoding, 内嵌页面中, 不再需要发送请求

3. 针对弱网, 不自动加载图片,, 只显示本占位符

首屏优化方案

首屏时间 > 白屏时间

  1. 首屏内容现实结束, 才认为加载完毕, 页面可用

  2. 一般情况首屏展示的页面都非常重要, 需要弱网情况下可用;

标准: 秒开率 > 中位数 > 平均值;

白屏时间短, 首屏时间长, 首屏时间可以再拆分:

白屏时间, 数据响应时间, 图片资源加载.

懒加载

通过使用懒加载,可以最大限度降低了数据接口传输阶段的时间。

缓存

赋予页面二次访问不需要重复请求的能力。接口缓存和静态资源缓存起到中流砥柱的作用。

接口缓存

如果使用 H5 请求数据,必须等 WebView 初始化之后才能请求(也就是串行请求),而 Native 请求时,可以在 WebView 初始化之前就开始请求数据(也就是并行请求),这样能有效节省时间。

也可以借助 SDK 封装来实现WebView 拦截请求

静态资源缓存

资源长期不变的话,我们可以使用强缓存,如 Cache-Control 来实现。

如果资源本身随时会发生改动的,可以通过设置 Etag 实现协商缓存。

离线化

离线化是指线上实时变动的资源数据静态化到本地,访问时走的是本地文件的方案。

把页面内容静态化到本地。

如果需要登录, 及支持 SEO 功能, 可以打包构建时预渲染页面,前端请求落到 index.html 上时,已经是渲染过的内容。此时,可以通过 Webpack 的 prerender-spa-plugin 来实现预渲染,进而实现离线化。

并行化

并行化则是在请求通道上功夫,解决请求阻塞问题,进而减少首屏时间

混合应用

App 启动阶段的优化方案

WebView 全局的优化

页面白屏阶段的优化方案

离线包方案

骨架屏

SSR

首屏渲染阶段的优化方案

预加载方案

通过客户端代理数据接口请求,在客户端初始化 WebView 的同时,直接由 Native 发起网络请求,H5 页面初始化完成后(对于 CSR 页面,也就是 index.html 加载完成后)直接通过 SDK 向 Native 获取数据。

根据业务场景选择预加载。提前加载一页的展示数据.

会根据用户的操作路径,判断打开搜索结果页的概率。如果概率超过某一个值,就会启动搜索结果页的数据获取。

WebView 的优化,全局 WebView Pool 时一定要注意及时销毁,不然对 App 资源的占用会比较大。

很多公司在预加载数据的基础上发展出了预渲染,但在实施过程中我们发现,它对 App 内存占用过大。

离线包方案

离线包生成

可以通过webpack 插件 ak-webpack-plugin 实现

第一步,将前端项目从 Git 仓库中 clone 出来,然后打出一个 offline 分支。

第二步,拷贝离线包专用的 webpack 配置文件到项目中,我们在此开源项目的基础上做了一些修改。比如,修改对应的 package.json 文件,这样在本地测试时可以直接将离线包拷贝到测试机上,以方便进行测试。

"builduploadtest": "node build/uploadtest.js",

"buildupload": "node build/upload.js”,

第三步,通过 npm i 安装所需的包,并执行命令 npm run build 查看效果,然后同步修改config/offline.js 中的对象 URL 为页面真实 URL,修改导出静态资源的路径为真实的 CDN 资源路径。线上资源路径和离线包资源路径映射对应。

如果一些资源不需要走离线包,比如非首屏的图片资源,可以选择 webpack 排除的方式设置。

离线包管理

离线包的类型一般包括差分和全量包,

那怎么实现差分包呢?可以使用 BSDP . 它有两个核心模块:bsdiff 和 bspatch。

其中 bsdiff 是个库函数,用于对源文件和目标文件求 diff,生成差分包。

而 bspatch 主要是用来根据差分包的内容,合并本地版本成一个新的全量包。

离线包部署及优化

将前端工程打包,生成离线包的入口页面 index_sonic.html (支持离线包的index.html),然后通过前端的静态资源发布系统上线到 CDN。

接下来,FE 将静态资源(如 index.js、home.css、banner.jpg)打包成全量离线包到 CDN,然后同步增加离线管理后台的配置,离线管理后台会根据基础包生成差分包上传到 CDN。

离线包问题

离线包的开关功能。在出现问题时,通过在离线包后台操作,及时关掉离线包功能,就可以及时确保用户功能恢复正常。

离线包命中率的统计,因为离线包即便不命中也不影响页面效果,所以出现问题很难发现,为此,在业务上线的日常运行中要对命中率进行统计。

在 iOS 系统,我们经常会用到 WKWebView ,此时如果要实现离线包,必须解决 WKWebView 下面的请求拦截难题,这时可以借助私有 API 方案来实现。

问题的诊断定位流程和原来不一样了。平常的定位问题只需要抓包,查看 source 即可,而在离线包的问题诊断,需要我们先抓包getofflineconfig 接口,找到对应的 bid,然后根据 bid 找到正确的配置项,点击配置项进入详情下载离线包,最后解压离线包确认代码是否正确。

难以100% 覆盖

残留离线包问题

服务端渲染

利用服务端的性能优势,尽量在服务端完成资源加载、首屏切分等工作

利用服务端统一缓存机制,对数据接口、页面和组件做缓存

第一,服务端渲染的最大优势,就是后端服务性能要远高于手机,所以请求数据接口和渲染时,我们可以把很多原本客户端做到的事情挪到了服务端,比如模块文件加载,首屏切分等。

第二,服务器端缓存与客户端最大不同是,服务端属于统一公用,也就是说,只要某一个用户访问过一次,后续所有用的访问都可以使用这份缓存。我们可以利用这一特点,采用 LRU(Least Recently Used,最近最少使用缓存机制)和 Redis 做好缓存功能,降低白屏时间。

具体来说,LRU 属于页面级缓存,对于数据统一性页面(有别于千人千面数据的页面),利用 LRU-Cache 可以缓存当前请求的数据资源。为了降低缓存的颗粒度,提高缓存的服用行,我们还可以用它来对渲染后的 vue 组件进行缓存。

而使用 Redis 可以对跨页面的数据接口进行缓存,将整体渲染时间再减少 100ms。为什么呢?因为 SSR 应用程序部署在多服务、多进程下,该进程下的缓存并不是共享的,这就造成缓存命中效率低下,而使用 Redis 可以解决这个问题,进而更好实现跨页面数据缓存(关联上跨云接口缓存这里,呼应主题)。

WebView 性能优化

并行初始化

所谓并行初始化,是指用户在进入 App 时,系统就创建 WebView 和加载模板,这样 WebView 初始化和 App 启动就可以并行进行了,这大大减少了用户等待时间。

如果是使用 native 开发的应用,根据用户在首页的访问路径,选择初始化策略,操作体验会更好。以携程 App 为例,假设用户进入首页后,停留在西双版纳自由行区域,直接加载 WebView 和模板,两者同时运行,此时首屏主要工作就变成加载接口请求数据和渲染模板部分的工作了。

为了减少 WebView 再次初始化的时间,我们可以在使用完成后不进行注销,将里面数据清空,放进 WebView 池子里面,下次使用时,直接拿过来注入数据使用即可。注意,使用时,要对 WebView 池子进行容量限制,避免出现内存问题。

另外还需注意一点,由于初始化过程本身就需要时间,我们如果直接把它放到 UI 线程,会导致打开页面卡死甚至 ANR(Application Not Responding,应用无响应),所以,我建议将初始化过程放到子线程中,初始化结束后才添加到 View 树中。

资源预加载

资源预加载,是指提前在初始化的 WebView 里面放置一个静态资源列表,后续加载东西时,由于这部分资源已经被强缓存了,页面显示速度会更快。那么,要预加载的静态资源一般可以放哪些呢?

一定时间内(如 1 周)不变的外链;

一些基础框架,多端适配的 JS(如 adapter.js),性能统计的 JS(如 perf.js)或者第三方库(如 vue.js);

基础布局的 CSS 如 base.css。

一般在 App 启动时,系统就加载一个带有通用资源模版的 HTML 页面,虽然这些静态资源不经常变化,但如果变化呢?怎么避免因变化导致 App 频繁发布版本的麻烦呢?

一个办法是通过静态资源预加载后台进行管理。具体的话,我们不需要从 0 到 1 搭建,只需要在离线包后台添加一个栏目即可。

在业务接入预加载功能时,前端工程师通过静态资源预加载后台发布出一个静态资源列表页,然后把它的 URL 提供给 App,App 启动时会对这个 URL 下页面中的静态资源进行预加载。之后,前端工程师就可以查看静态资源的编号 ID、URL 和类型,进行删除、添加等管理操作。

数据接口请求优化

数据接口请求优化,主要是通过同域名策略和客户端代理数据请求来实现。

其中,同域名策略是指前端页面和资源加载,尽量和 App 使用的数据接口在同一个域名下,这样域名对应的 DNS 解析出来的 IP,由于已经在系统级别上被缓存过了,大大降低了加载时间。

客户端代理数据请求,则是指把前端的数据请求拦截起来,通过客户端去发送数据请求。因为正常的页面加载顺序是,前端在 HTML,CSS,JS 拉取下来之后才开始由 JS 发起前端的 ajax 请求,获取到数据后程序才开始进行填充。而我们通过客户端代理数据请求,可以把前端的 ajax 请求提前到与页面加载同时进行,由客户端请求数据,等 H5 加载完毕,直接向客户端索要即可。如此一来,便缩短了总体的页面加载时间。

注意,这里的数据拦截环节,Android 端可以重写WebViewClient的shouldInterceptRequest 方法,iOS 端没有类似的方法,只能通过私有 API 方案、自定义协议方案和 LocalWebServer 来实现。

前端架构性能调优

前端架构性能优化,是指通过在前端开发、编译、打包发布环节所作的优化,以此来提升前端性能的方案。因为我们比较关注首屏时间,对这方面贡献比较大的是开发和打包发布这两个环节,

长列表性能优化

打包优化

打包优化方面,我们可以通过 webpack 插件来完成。我们可以使用一个 webpack 插件——webpack-bundle-analyzer,通过它可以对打包结果进行可视化分析。

采用预请求、预加载和预渲染

预请求

想要通过拉取后端接口来降低首屏时间,我们需要先实现接口的预加载。而实现它要先解决预请求的逻辑,也就是统一拼装请求参数的逻辑。

前端应用通过解析页面 URL 路径,拿到所需的一些参数, 然后调用 Native 的 schema 进入参数解析环节,找到 Native 对应的协议和参数,然后再通过参数初始化,拼装成对应的参数。

上述流程面临的一个问题是,没有预请求的页面 URL 参数,也没法通过 Native 获取到。这需要自己根据逻辑拼装,所以往往会单独做出一套流程,结果就是不但容易出错,还会因为需要用类似两份代码去实现这个功能,反过来拉长页面的首屏时间。所以可以将预请求封装成preReq 功能,把所有的功能都包括起来,用同一份代码实现。

在做完这个统一拼参逻辑后,预请求实现起来就容易了。具体来说,如果你已经使用了 Native 统一请求,直接走客户端逻辑发送即可。如果还没有走 Native 统一请求,可以通过对原生请求进行封装拦截的方式实现。

预加载

在完成预请求参数拼装之后,紧接着就是预加载逻辑了。首先是要把握预加载的时机。以机票列表页为例,我们需要判断用户操作的特定路径。如果用户操作命中了这个特定路径,就会做预加载,去请求列表页的接口。

预加载也要做好缓存处理。 我们要先在内存里面 check 一下是否存在之前预加载的数据。有的话,直接用预加载数据,做后续操作,如果没有,就继续走预加载逻辑,然后设置缓存数据。

预渲染 NSR

预渲染是指在用户访问这个页面之前,完成页面渲染的准备。前端进行判断并会把搜索结果页先渲染出来,只不过在可视区域下方,用户是不可见的。

当用户点击开始搜索时,前端会去 check,如果已经有了预渲染的页面,只需要把页面显示出来的操作, push 到顶层即可。这样就省去了初始化页面、请求数据和渲染的时间。

这就需要用到 “客户端”渲染技术了。它有别于 CSR,而是 NSR(Native side rendering,客户端渲染),即通过客户端(Native 侧)进行页面结构拼接,进而实现页面渲染的处理技术

NSR 优化时,需要离线包提供模板等资源(如 HTML、JS、CSS ),预加载提供数据,把页面作为数据经过模板函数变化后产生的结果,然后通过 v8 引擎在客户端渲染出来。

在 Native 侧实现一种类似前文 SSR 方案的 Native 本地渲染服务。

实现完 NSR 之后,业务就可以使用预渲染功能了。在使用时,前端代码不需要做什么改动,业务侧前端工程师接入 NSR,把后置流程准备好就可以了。所谓的后置流程,就是指渲染好下级页面后放置在可视区域之外。

预渲染,有时会遇到内存问题,我们可以精简预渲染的内容,比如一些图片资源,可以延后获取。

一些基础方案

减少合并资源, 减少请求, 数据缓存. 防布局抖动, html 优化, css 优化, 图片加载优化.缓存请求, 服务端相应优化, 页面解析与处理, 静态资源优化, 静态资源强缓存方案, DNS预解析处理, 懒加载, 单页应用.

性能优化体系:

  1. 性能优化流程: 性能指标设定 性能标准确定, 受益评估, 诊断清淡, 优化手段. 性能立项, 性能实践

  2. 性能指标采集与上报: 将性能指标以代码的形式落地, 确保可以采集, 然后在sdk封装后集合统计埋点, 最后根据实际情况制定上报策略;

  3. 性能监控预警平台: 性能上报处理后台和性能可视化展现前台;