会员首页性能优化实践

488 阅读6分钟

项目背景

会员首页是整个会员的承载页,项目中集合了会员身份、金币、任务、cps、各种弹窗(至少8个),整个页面还要支持运营日氛围配置。

此项目有以下特性:
  • 接口请求数量之多
  • 页面弹窗多,并指定优先级
  • 运营氛围支持
  • 多页面

面临的问题

随着版本的迭代,项目体积越来越大,页面加载速度越来越慢。

优化目标

提高加载速度,增加用户体验

优化前构思

通过对项目分析,大致从以下几个方向入手:

  • 减少打包js体积
  • 减少首屏资源的加载数,把其它非首屏资源渲染时机后置
  • 对不敏感的数据增加一层缓存
  • 减少白屏时间,提高用户体验

既然有了目标那就搞起~。

PS:本次主要针对项目本身做的优化,不涉及服务器、App相关内容。

具体技术实现

通过上面的分析,下面一一对应着分析

一、减少打包js提交

由于项目创建的时候使用的老版本的create-react-app构建的,在拆包上没有得到很好地集成,所以在用webpack构建包的时候,直接将每个页面都输出到了一个比较大js,在本着最小改动的情况下做了以下的事:

import React from 'react';
export const asyncComponent = loadComponent => (
    class AsyncComponent extends React.Component {
        constructor(...args) {
            super(...args);
            this.state = {
                Component: null,
            };
        }
        componentWillMount() {
            if (this.hasLoadedComponent()) {
                return;
            }
            loadComponent()
                .then(module => module.default ? module.default : module)
                .then(Component => {
                    this.setState({
                        Component
                    });
                })
                .catch(error => {
                    /*eslint-disable*/
                    console.error('cannot load Component in <AsyncComponent>');
                    /*eslint-enable*/
                    throw error;
                })
        }
        hasLoadedComponent = () => {
            return this.state.Component !== null;
        }
        render() {
            const {
                Component
            } = this.state;
            return (Component) ? <Component {...this.props} /> : null;
        }
    }
);

通过上面asyncComponent组件动态import(/* webpackChunkName: "Index" */)引入组件,此种方式将在构建的时候会将每个路由生成一个对应的js,那我们分析一下打包之后的js包含的内容

image-20200119173849306

不难发现,在用到swiper的页面,打包的时候每个js里面存在,而且自己项目中的一些工具类也被分散到了5.chunk.js和6.chunk.js,这不能容忍重复引用。

经过对webpack的文档的查找,找到CommonsChunkPlugin这个插件,可以抽取chunk的公共代码,具体代码如下:

new webpack.optimize.CommonsChunkPlugin({
            // name: 'lib',
            async: true,
            chunks: [
                'Index',
                'ScoreDetail',
                'Description',
                'GrowthValue'
            ],
            minChunks: (module) => {
                let context = module.context;
              	//context是每个文件的路径,通过检索目录抽出某个目录下面js
                return context && (context.indexOf('memberCenter/src/lib') >= 0 || context.indexOf('node_modules/swiper/dist') >= 0);
            }
        }),

再看一下我们打包输出js,可以看到swiper,和一些工具类被打包到了一个0.chunk.js里面,Index.chunk.js的题解也相对减少100kb,如下图:

image-20200119173525614

后面还会对这一块做进一步优化,按楼层进行更细粒度的拆分。

二、减少首屏资源的加载数,把其它非首屏资源渲染时机后置

大家都知道其实首屏加载的内容是有限的,所以我们把首屏能展示的楼层一次加载出来,其余不展示的楼层我们采用异步加载,当用户滑动到指定位置的时候才会加载,这样子达到了按需触发异步加载,

三、对不敏感的数据增加一层缓存

大家手机上应该都安装一些新闻之类的app,在我们首次打开的时候回发现貌似不需要请求数据就直接展示了了新闻,而且这些新闻好像之前都看过,是的你没有看错,这就是app本地做的缓存机制,每次新闻加载完之后都会本地缓存当前查看的信息,下次进来的时候直接加载缓存数据,这样子提前了页面渲染时机,从而达到页面快速响应的目的,那我们也是借鉴这种思想本地也做了一层数据缓存,效果还不错。

现在react/vue都是基于数据驱动UI的模型,天然具备了对设置的数据做差分对比,所以这一块我们只要保证本地缓存能被更新就好。具体实现是通过md5对请求url以及参数加密之后作为缓存的key,保证key的唯一性以及减少因为参数变化导致取缓存错误。

取缓存:

let cacheKey = '';
let cacheData = {};
//缓存接口数据
try {
    const encryptStr = `${this._getBaseUrl()}${functionId}${functionId.indexOf('?') === -1 ? '?' : '&'}reqData=${paramsStr}`;
    if (cacheEnable) {
        //缓存key,使用原始参数生成
        cacheKey = md5((this.getCookie('user_id') || '') + encryptStr);
        cacheData = LocalStorageUtils.getValue(cacheKey);
        if (cacheData) {
            cacheData = JSON.parse(cacheData);
            successHandle && successHandle(cacheData.data);
        }
      }
} catch (e) {
}

保存缓存:

if (cacheEnable && JSON.stringify(cacheData) !== JSON.stringify(result)) {
    LocalStorageUtils.setValue(cacheKey, JSON.stringify(result))
}

PS:此处会看到会看到从cookie中取user_id这个属性,主要是为了防止同一个设备切换账号展示数据串了,这是我们不想看到的。

四、减少白屏时间,提高用户体验

白屏原因:
1. 不可避免的
  • 浏览其下载html的过程
2. 可优化的
  • html中引用了过大过多的js,或者做了比较耗时逻辑
  • react/vue渲染页面的过程
分析+处理方案:

现在react/vue渲染都是基于js动态渲染,它会将渲染好的UI插到html指定的元素中,此时就会出现如果渲染js的体积过大会导致加载时间较长,并且如果html中引用了一些比较大的也会导致html加载比较慢,就会到时白屏时间加长。前面已经缩小了页面js的体积,那边怎们就从怎么减少html中js的加载数,或者看看有没有一些js可以延后加载的。我们引入的乐高、cps的js就是这样子,不是每次进入都需要立刻加载,所以我们将其加载方式改成动态插入script的方式,具体代码如下:

/**
     * html 动态注入script标签
     * @param id
     * @param url
     */
    loadJS(id, url, callback) {
        const script = document.createElement('script');
        script.id = id;
        script.type = 'text/javascript';
      
        //(下面方法线上验证中,目前没有出现兼容问题)
        if (script.readyState) {
            script.onreadystatechange = () => {
                if (script.readyState == "loaded" || script.readyState == "complete") {
                    script.onreadystatechange = null;
                    callback && callback();
                }
            }
        } else {
            script.onload = () => {
                callback && callback();
            }
        }
        script.src = url;
        document.body.appendChild(script);
    },

那么我们减少了html中js的加载数量,那白屏怎么处理呢,我们借鉴了其它厂的方式,采用骨架屏占位的方式,在html加载完之后在最上层加一个base64骨架图片占位,给用户一种已经打开页面的视觉感,增加用户体验。

总结:

貌似到这也差不多了,还有一些页面中具体的逻辑细节优化,此处就不再赘。本次主要针对项目中的一些存在的问题做的优化,比较有针对性,有问题或者好的建议可与我联系,好了来看一下优化前后对比,无图无真相。

1、先看一下性能对比图如下:

处理前:

NetWork:Online

image-20200119180127893

NetWork:Fast 3G

image-20200120092649420

处理后:

NetWork:Online

image-20200119183840462

NetWork:Fast 3G

image-20200119183954488

2、视频效果对比

  • 4G环境-IOS

    • 会员首页-首次访问对比(左边是优化后,右侧是优化前)
    • 会员首页-2次访问对比(左边是优化后,右侧是优化前)
  • wifi环境-安卓

    • 会员首页-首次访问对比(左边是优化后,右侧是优化前)

    • 会员首页-2次访问对比(左边是优化后,右侧是优化前)