【性能优化实战】宝宝知道小程序FMP优化实录

背景

宝宝知道小程序从首次发布至今,通过了几十个版本的迭代。随着业务发展,页面功能内容的不断增多,相关性能数据不断变差,核心性能数据 FMP 长期处在 2000ms 以上。小程序

在该项目以前,咱们团队也对小程序作了必定的性能调优工做,内容包括:后端

  1. 包体积优化,去除了很多引用在项目中的图片素材文件,将包体积优化至 500kb 如下;
  2. 联合后端对耗时较高的业务接口作优化,单个接口返回速度须要控制在 100ms 左右;
  3. 优化了部分业务逻辑,小程序启动时减小了一些没必要要的操做逻辑;
  4. 使用了小程序框架提供的最新生命周期 onInit ,可提早 100ms 左右发起业务网络请求;
  5. 使用 prelink 预链接网络,提高数据接口的请求效率。

通过上述手段以后,FMP 降到了 1900ms 左右,后续再也没法产生优化效果。api

以上优化手段,基本排除了网络链接,包体积优化不到位引发的性能不佳。那么咱们就只有一个问题须要仔细排查 —— 内容的渲染效率。网络

问题发现

目前从手百上打开宝宝知道小程序的最大入口页面为问答页,总体 pv 占比超过 6 成,那么咱们优先优化这个页面,即可以带来性能收益的最大化。框架

通读问答页代码,按显示顺序从上到下,整个页面的功能点依次为:异步

  1. 直播信息横条
  2. 问题区
  3. 回答区
  4. 广告组件区
  5. 为你推荐 feedlist

须要展示的内容类别不少,内容信息量较为庞大。部份内容须要单独接口获取,外加上引入的广告组件,展示效率彻底没法优化。ide

由于以上业务内容的展示须要,在加载时,使用 setData 触发内容渲染,会形成较大问题,好比:工具

  1. 加载期间调用 setData 的频次过多,onLoad 时会 set 、onShow 时会 set ,不一样阶段发起的异步数据加载后也会 set 。当前线程内同时的屡次 setData ,极易形成小程序渲染线程拥塞,影响内容渲染效率
  2. 单次 setData 数据量过多,接口数据返回后,全部页面内须要的数据都一次性被提交到渲染线程中渲染,致使线程等待时间长,影响了有效内容的最终展示。虽然减小 setData 调用次数是官方提倡的,可是单次提交过多数据渲染,也并非最优的策略。

以上两条 setData 的使用问题,在配置较好的手机设备上,并不会体现出问题,可是对于中低配置的手机设备,由于操做拥塞或大量数据渲染操做带来的渲染延迟,形成的用户体验损失仍是很大的。性能

优化前的问答页数据渲染示意图

image

优化以前,页面加载完数据以后的首次渲染,会一次提交问题区、回答区、广告组件区三个部分的渲染任务,因为这三个区域涉及的内容量比较大,基本都会超过一屏,甚至两屏以上,另外各个区域也都包含一些图文内容,加上自己耗时较高的广告组件。总体页面内容渲染速度不好。而且,由于存在直播信息横条等单独异步请求加载的数据内容渲染,也容易形成 setData 操做在小程序渲染线程中拥塞现象的发生。优化

因此,从小程序 FMP 的统计规则来看,目前的数据渲染逻辑,显然并非最优的。

既然 FMP 主要统计的是用户第一眼能够看到的首屏位置内容,那么咱们是否是能够换个思路来完成咱们的内容渲染工做。

在确保数据接口性能已经符合常规标准的状况下,咱们可使用更聪明的渲染策略。

优化方案

为了解决上述问题,咱们构思了一套分屏式内容渲染策略,意在让用户能最快速度的先看到一部分关键内容,再分阶段渲染剩下须要被渲染的数据,而那些不须要被自动渲染的数据,能够改为由用户某种行为(好比滑动页面)触发加载和渲染。

优化后的问答页渲染示意图

image
PS:广告组件自己为异步组件,第二次 setData 会触发广告组件渲染,而广告组件内部自行发起异步内容的加载。

优化后的问答页渲染逻辑,总体上被拆分为四个阶段:

  1. 核心内容快速渲染阶段。该阶段为 FMP 主要检测的数据渲染时长,因此在这个阶段,咱们须要让页面的内容和元素,足够装满一屏。
  2. 核心内容补全渲染阶段。该阶段将核心内容中存在的耗时内容,好比图片、视频以及小程序 native 组件等内容渲染上屏(注:关于渲染比较耗时的组件,目前已知视频 video 、全部小程序 native 组件,都不适宜放在第一阶段直接渲染,图片 image 若是条件容许,也尽可能不放在第一屏)。
  3. 后续内容渲染阶段。该阶段将本次接口返回的须要渲染的数据所有上屏。
  4. 其余非主要异步数据渲染阶段(图例中的直播信息横条)。将另一个接口的数据渲染上屏。

PS:若是存在核心内容渲染完成后依旧没法撑满一屏的状况,能够考虑设置总体页面 min-height:100vh ,或者页面下方放置占位元素,来达到撑满一屏的效果。

优化成果

该优化版于2020年8月4日上午11点左右全量上线,在手百中逐步放量。 FMP 指标在8月5日和6日两天快速降低,7日逐步稳定。总计优化 FMP 指标 540ms 。

image
从数据表现来看,优化效果很是明显。

而且,问答页做为宝宝知道小程序 pv 最大的落地页,占据总 pv 的 60% 左右,另外还有 40% 的其余页面须要咱们持续优化,将来数据表现还有不小的优化空间。

工具建设

工欲善其事必先利其器。后续咱们还须要优化其余入口页面的性能,以及为后续开发高性能页面作持续的技术储备,因此咱们将开发中遇到的和性能有关的问题作了一些抽象,经过打造基础操做的工具类库,从底层上来解决或者规避问题。

上文中有提到,同时发起多个 setData 操做,极易形成小程序渲染线程的拥塞,致使渲染效率受到影响,下降小程序内容上屏的效率。实际开发中,咱们若是要避免同时发起多个 setData ,必然会带来额外的逻辑思考成本和代码结构调整的成本,也容易由于调整,下降代码的可读性和可维护性。为了兼顾渲染性能的须要和代码结构的可读性,以及代码观感,咱们专门设计了一个内容渲染任务管理器。

DataSetter

DataSetter 目前已经集成在团队内部的小程序工程脚手架中,经过 AdvancedPage 建立的小程序 Page 实例,便可支持经过该管理器开放的 api 接口,向小程序的渲染线程提交数据渲染任务。

DataSetter 将小程序 setData 操做封装为一个队列式的渲染任务管理器,使用 DataSetter 进行 set 数据操做,可使得单位时间内只有一个 setData 操做被执行,而其余被同时 set 的数据,将在队列中排队依次执行。

图例:优化前同时 setData ,会致使小程序渲染线程的拥塞

image

图例:优化后同时 set ,DataSetter 会总体管理数据渲染任务,不会形成渲染线程拥塞

image

为了支持分屏式渲染策略的编写,DataSetter 的 API 被设计为链式调用式设计。能够以非嵌套的方式编写N阶段内容渲染逻辑,代码行文清晰易懂。

this.$dataSetter.set({
    // 第一阶段渲染数据
    status:'success',
    aaa:111
}).done(e => {
    // 第一阶段渲染完成
    console.log('第一阶段渲染完成');
}).set({
    // 第二阶段渲染数据
    bbb:222
}).set({
    // 第三阶段渲染数据
    ccc:333
}).done(e => {
   // 第三阶段渲染完成
   console.log('第三阶段渲染完成‘);
});
复制代码

DataSetter 源码

/**
* @name DataSetter
* @description setData语法加强,支持链式调用和队列式set数据,一次setData成功以后才开始下一次setData
*/
class DataSetter {
    queue = [];  
    context = null;  
    index = 0;  
    constructor(context) {
        this.context = context;
    }  
    set(dataset = {}) {
        this.queue.push({dataset, callback: null});
        if (this.queue.length === 1) {
            this.exec();
        }
        return this;
     }  
     done(callback) {
         this.queue[this.queue.length - 1].callback = callback;
         return this;
     }  
     exec() {
         let task = this.queue[this.index];
         if (!task) {
             // console.log('all task done!');
             this.refresh();
             return;
         }  
        const next = () => {
            // console.log(set data ${this.index} ok!);
            task.callback && task.callback();
            this.index++;
            this.exec();
        };
        // 若是当前任务dataset为空,则不调用原生setData,直接执行回调
        if (!task.dataset || Object.keys(task.dataset).length < 1) {
            next();
            return;
         }
        // console.log(set data ${this.index});
        this.context.setData(task.dataset, next);
     }  
     refresh() {
         this.queue = [];
         this.index = 0;
     }
}  
// Page Demo  
Page({
    $dataSetter: null,
    onLoad() {
        this.$dataSetter = new DataSetter(this);
    }
});
复制代码

后记

形成小程序性能不理想的状况有不少,而渲染问题的解决和优化是能够带来最大收益的,而且若是能根据实际的业务场景,来灵活设计视图的渲染策略,每每能够带来奇效。渲染问题优化是一件很是精细的事情,尤为是面对逐渐复杂的业务代码,勇于去改造尝试,才是最终成功的起点。

相关文章
相关标签/搜索