基于VUE的SPA单页应用开发-加载性能篇

一、基于异步数据的vue页面刷新

先看看基于异步数据的vue页面刷新后,都发生了啥~css

如图所示:
html

图1 基于异步数据的vue页面刷新 网络请求图vue

步骤以下:ajax

  • step1:请求页面;
  • step2:请求页面内的css、js资源;
  • step3:vue页面初始化;
  • step4:页面渲染,框架呈现[无数据内容];
  • step5:请求页面实际数据;
  • step6:数据ready,填充视图,图片资源加载;
  • step7:完整页面呈现。

步骤分析:vuex

  • step1:请求html文件;promise

  • step2:请求资源;
    优化点:
    • a、屡次访问的资源缓存:可从MD五、组件打包方式等角度再细分;
    • b、app框架资源预加载:若是是hybird开发的app,可经过app框架预加载的方式,将单页应用的资源提早缓存。
      单页的css、js资源,与传统页面的资源相比,规模要大不少。其集合了几乎单页应用的全部css、js文件,随着应用的规模大小成正比增加。合理的缓存处理,将大大提高页面加载速度。a、b两点可实现性能加速的缘由是,本地加载过的资源,会缓存在本地;页面请求资源时,浏览器会先查找缓存,若是有缓存,则本地取,节省了网络请求。[可了解浏览器的强缓、弱缓]
  • step3:页面初始化;
    优化点:
    • 利用v-if指令按需加载组件~
      因为vue在初始化过程当中,会深度查找子组件,生成依赖,构建虚拟DOM,因此其初始化时间相对较长;不过在查找过程当中,遇到v-if为false的组件,将中止深度查找,从而节省初始化时间。由此可经过控制v-if的布尔值,实现性能优化和组件的按需加载。例如在初始化过程当中,仅首屏必须的组件v-if=true,其余组件[如非首屏组件、弹窗组件等],可在下一事件循环中[经过setTimeout(fn, 0)等异步操做可实现],或者首屏资源加载完成后,开启。
  • step4:默认数据页面渲染;(默认数据由vuex提供)
    优化点:无数据的页面框架的渲染展示[一般所说的灰框],让用户提早感知页面,从而提高用户体验;(包括读取vuex数据,进行渲染)浏览器

  • step5:异步请求数据,与step4同时进行,经过ajax实现;缓存

  • step6:资源加载;
    优化点:进行图片[视频]分批加载优化,从而增长同一带宽下单图的加载速度,加速首屏展示。
    建议图片加载流程:
    • step6-1:首屏展示必须图片加载及首屏默认图片;[如:banner图第一张];
    • step6-2:首屏其他图片加载及其余默认图片;[如:剩余的banner图];
    • step6-3:非首屏 or 弹窗。[接下来的加载顺序,可根据需求调整。但要遵循一条原则,不影响首屏的用户交互。]

二、基于异步数据的vue页面的路由跳转

再来看看经过单页路由跳转到新页,又发生了什么?
性能优化

图2 基于异步数据的vue页面路由跳转 网络请求图网络

页面加载步骤:

  • step1:捕获到路由变化;
  • step2:初始化该页面,并默认数据渲染;(包括读取vuex数据,进行渲染)[step三、step4]
  • step3:异步请求最新的初始化数据;[同step5]
  • step4:资源分批预加载。[同step6]

总结:

  • 优点:页内跳转性能很是赞。对比图1和图2,在路由内跳转时减小了图1中step一、step2的页面请求和.css、.js的请求时间[节省1s+],页面展示嗖嗖的。再好好结合vuex的数据流,能够给用户很是棒的体验。

  • 劣势:再观察图1的网络请求图,能够发现如下几点:
    • a、css、js资源相对传统页面,量更大,加载时间加长;
    • b、vue的首屏展示,依赖异步数据的请求,相对传统同步页面,增长了单独的数据请求时间消耗;
    • c、页面渲染,在js执行完毕以后,才开始进行;而最终的首屏展示,则须要等待异步数据请求到达以后。
      因为a、b的存在,c的首屏展示时间相对传统页面更慢。

VUE的异步单页应用优点与劣势很是明显,缺点是初始化时间长,依赖js资源的加载;优点是运行速度快,路由内跳转几乎没太多的时间消耗。若是是必定规模大小的单页应用,它将是不错的选择。特别是使用hybird开发,经过app框架将资源预加载以后,需依赖js资源的劣势也必将不存在,那将给到用户传统页面没法给到的体验。

Q:那有什么办法来解决这些劣势吗?
A:在接下来的3中,将提出一种解决方案。

三、提速方案

对于a点,资源量大,能够从打包方式、缓存、CDN分发等角度进行处理;
对于b点,有两种方式解决:

  • 一、同步+异步数据请求:刷新页面时,使用同步MVC框架的方式,经过后台路由带入初始化数据;页内路由跳转时,仍然采用异步的方式进行。
  • 二、异步数据请求提早:刷新页面时,将数据请求提早至js资源加载前,因为网络请求可并发多个,将节省单独的数据请求时间。

tips:若是不是mvvm的异步单页,推荐使用同步+异步的方式,页面的展示能够提早至js资源加载以前。[因为mvvm框架下的页面视图经过数据进行驱动,该驱动的基本须要依赖js脚本实现,因此必须等待js加载完毕,才能正确展示页面。所以,在mvvm框架下,同步+异步的方式仅能节省数据请求时间,但其余单页应用能够节省数据请求时间+js资源加载时间]
ps:js的加载顺序:不影响页面初始化呈现的js底部后置:如日志、分享、im的相关js。

四、初始化性能优化[可用于加速首屏呈现]

以下示例:

<app>
    <app-nav></app-nav>
    <app-content></app-content>
    <app-sidebar></app-sidebar>
</app>

其页面结构与组件结构关系图以下:

vue初始化的组件编译原则是,按照深度查找,遇到v-if为false的节点或者叶子节点,中止查找。从示例的组件结构图,咱们能够看出,
初始化中组件查找过程为:

  • step1:首先查找根节点的子组件nav组件,其为叶子节点,编译,返回;
  • step2:查找app的第二个子组件(节点)content,其非叶子节点,且无v-if标记,继续深度查找;查找其子节点c-a组件,为叶子节点,编译,返回content;查找另外一个子节点c-b组件,叶子节点,编译,返回;
  • step3:节点content查找完毕;返回app,查找sidebar节点,sidebar非叶子节点,且无v-if,继续深度查找;同step2,最终返回app节点。
  • step4:全部组件编译完毕,初始化完毕,渲染。
    如上示例,将深度遍历全部子组件,再完成渲染。若是将首屏不须要展示的组件设置成v-if,将下降深度查找的复杂度,从而加速组件初始化,加速页面的呈现。

加速代码以下:

<app>
    <app-nav></app-nav>
    <app-content v-if="showContent"></app-content>
    <app-sidebar v-if="showSidebar"></app-sidebar>
</app>

new Vue({
    data: {
        showContent: false,
        showSidebar: false
    },
    created () {
        // 显示content
        setTimeout(() => {
            this.showContent = true;
        }, 0);
        // 显示sidebar
        setTimeout(() => {
            this.showSidebar = true;
        }, 0);
    }
});

以上代码将组件content与sidebar的v-if设置成false,组件的编译查找过程以下:

  • step1:不变;
  • step2:查找content组件,其为v-if=false,中止,并返回根节点;
  • step3:查找sidebar组件,其为v-if=false,中止,并返回根节点;
  • step4:全部组件编译完毕,初始化完毕,渲染。

是否是快了不少~~由此,首先被渲染,出现的是nav组件结构;另外两个组件经过showContent,showSidebar控制。

为什么这里使用setTimeout(fn, 0)控制两个组件的状态变化呢?

由于setTimeout是时间异步处理模块,经过其设置,相应的处理方法将在下一事件循环中才被执行,而VUE的渲染时机为本次主线程执行完毕。如此,使得另外两个组件的编译推迟到首次渲染以后,从而实现组件加速。

tips:这里须要注意,不是将组件设置v-if=false就能够了,要看看v-if=true的开启时机,若是是同一事件循环中被开启,便没有意义了。由于vue的数据驱动渲染时机,是同一事件循环中的代码所有执行完毕以后,拿到数据的最终状态才进行。同时,setTimeout(fn, 0)也不可乱用。

附加:图片分批预加载的js脚本

参数说明:
auto: 是否自执行
imgs: 需预加载的图片列表,为二维表
ignore:在自执行过程当中,须要跳过的图片批次脚标
firstSetReady: 第一组图片完成加载之后,置为true,便于外部掌握状态[通常首屏资源为第一组图片]
finished: 全部图片资源加载完毕
说明:
非自执行的需求,可直接调用loadOneSetImages方法,返回值为promise

let co = require('co')
class Preload {
    // 定义构造函数的数据结构,建立实例对象时,自动初始化
    constructor(auto, imgs = [], ignore = []) {
        this.imgs = imgs;
        this.ignore = ignore;
        this.auto = auto;
        this.firstSetReady = false;
        this.finished = false;
        this.init();
    }

    // 初始化函数
    init() {
        let me = this
        // 若是自动执行,则调用co模块,自动加载资源
        if (this.auto) {
            // generator的自执行函数,资源加载完毕时,参数finished置为true
            co(this.autoExeImageStream.call(this)).then(function() {
                console.log('资源加载完毕~')
                me.finished = true
            }).catch(function() {
                console.log('资源加载出错~')
                me.finished = true
            })
        }
    }

    // 同步加载分批图片资源,使用generator函数,完成当前批次加载,再启动下一批次的加载
    * autoExeImageStream() {
        let rstList = []
        for (let i = 0; i < this.imgs.length; i++) {
            if (this.ignore.indexOf(i) == -1) {
                yield this.loadOneSetImages(this.imgs[i], i)
            }
        }
    }

    // 加载一个批次的图片资源,当前批次全部图片加载完毕后,将firstSetReady置为true;表示第一组图片加载完毕
    loadOneSetImages(imgList, i) {
        let promiseList = []
        let me = this
        imgList.forEach(function(item, index) {
            promiseList.push(me.loadSingleImage(item))
        })
        return Promise.all([...promiseList]).then(function() {
            me.firstSetReady = true
        }).catch(function() {
            me.firstSetReady = true
        })
    }

    // 加载图片资源的函数,每一个图片单独设置primise
    loadSingleImage(src) {
        if (!src) {
            return new Promise(function(resolve, reject) {
                resolve('noImage')
            })
        }
        let newImg = new Image()
        newImg.src = src
        return new Promise(function(resolve, reject) {
            newImg.onload = function() {
                this.total++;
                resolve('success')
            }
            newImg.onerror = function() {
                reject('fail')
            }
        })
    }
}
export default Preload;

调用:this.preloadObj = new Preload(true, imgs)

图片分批预加载脚本经过ES6 promise结合generator函数实现,使得图片按照咱们想要的方式顺序加载,且单批次速度更快。但不能滥用,须要留意图片资源的加载规模与用户交互操做之间的关系。因为,ES6 的 promise在事件循环中的消息处理级别高于DOM事件[or 网络请求等其余异步模块],当有 promise 消息要处理时,其余事件消息将等待,直到promise处理完毕。所以在使用其进行预加载时,必定要结合业务状况及预加载的需求进行设置。若有须要能够建立多个preload实例,经过必定的条件,触发图片预加载,将其进行分散,从而达到性能加速的同时,用户交互体验也不受影响。

相关文章
相关标签/搜索