性能优化之组件懒加载: Vue Lazy Component 介绍

37,406 阅读11分钟

这篇文章分享了从遇到前端业务性能问题,到分析解决并且梳理通用的Vue 2.x 组件级懒加载解决方案(Vue Lazy Component )的过程。

初始加载资源过多

问题起源于我们的一个页面,下面是这个页面的截图和初次请求的瀑布图。

初始加载了155个请求
初始加载了155个请求

初始加载的时候,一共请求了155个资源,请求的瀑布图就快要和页面一样长了😊

请求概况
请求概况

初始加载的资源过多导致在 domInteractive 之后,页面花费了大量时间加载子资源,导致页面的 load 时长被严重拖长,达到了 5.6s 。

PerformanceNavigationTiming 信息
PerformanceNavigationTiming 信息

来看看这些子资源都是什么,根据请求资源的类型,我们找到了最多的类型是图片,这是显而易见的,页面上到处都是大图片,其次是 js 文件,由第三方的业务插件和一些 JSONP 的接口组成。

请求资源类型分布
请求资源类型分布

问题分析

页面结构拆分
页面结构拆分

再回到最初的这个页面,结合上面的数据情况我们得出了这个页面的问题总结结论:

  • 页面由大量模块组成
  • 每个模块部分由首页自主维护,部分由业务方通过插件维护
  • 所有模块是同时进行加载
  • 模块中图片内容较多
  • 每个模块的依赖资源较多(包括js文件、接口文件、css文件等)

解决思路

我们提出了下面两个主要的解决思路:

组件化分治思想

为了方便后续的优化,我们必须要求每个模块之间降低耦合,将相关的逻辑(比如请求接口、请求相关的依赖资源)都封装在内部,在 Vue 里落实成组件的形式。

  • 将各模块拆分为组件粒度
  • 将组件依赖的资源全部封装在组件内部进行调用

加载优先级

在完成了组件化的拆分,确保模块之间不会互相影响和产生耦合之后,我们可以方面地调整加载策略。加载的策略是根据可见性来处理优先级问题。

  • 优先加载首屏可见模块
  • 其余不可见模块懒加载,待可见或即将可见时加载

有了上面的解决思路,我们开始思考具体的实现:

如何解决判断可见性问题?

从前我们都是通过监听滚动事件、resize 事件来判断模块是否可见,代码不仅繁琐,而且一不小心没有函数去抖就又可能导致严重的性能问题。

现在我们有了更好的选择—— IntersectionObserver API ,IntersectionObserver 允许你配置一个回调函数,每当 target ,元素和设备视口或者其他指定元素发生交集的时候该回调函数将会被执行。这个 API 的设计是异步的,而且保证你的回调执行次数是非常有限的,而且回调是会在主线程空闲时才执行,在性能方面表现更优,使用起来也更简单。

http://caniuse.com/#search=IntersectionObserver
http://caniuse.com/#search=IntersectionObserver

目前是现代浏览器支持,低版本浏览器可以通过 polyfill 兼容。

如何尽可能懒的条件渲染?

在解决了加载条件的判断之后,我们需要解决加载条件为假的情况下不去渲染、加载条件为真的时候才渲染的问题,这里的答案非常简单:使用 Vue.js 提供的 v-if 指令,就可以做到真正的惰性渲染。

如果可见后进行初始渲染,可见前如何显示?

如果在判断加载条件为假的时候,什么都不渲染,就会带来一系列问题:

  • 用户体验比较差,最开始是白屏,然后突然又渲染出现内容。
  • 最致命的是我们判断可见性是需要一个目标来观察的,如果什么不都渲染,我们就无从观察。

这里引入一个骨架屏的概念,我们为真实的组件做一个在尺寸、样式上非常接近真实组件的组件,叫做骨架屏。

骨架屏
骨架屏

骨架屏的作用有:

  • 提升用户感知体验
  • 保证切换的一致性
  • 提供可见性观察的目标对象

如何提升切换时的体验?

在真实组件开始渲染的时候,需要一定的时间和空间,时间指的是真实组件从创建到渲染的时间,包括请求接口、请求资源和渲染的时间,空间指的是页面布局中需要给真实组件留出刚好的位置,避免产生抖动。

这里我们可以使用 Vue.js 内置的 transition 组件自定义骨架组件和真实组件的进入和离开效果,通过合理的布局和定位,减少切换时的抖动,
通过设置过渡效果给真实组件留出一定的加载时间。

上面的问题都有了答案之后,我们很容易就可以实现一个通用的方案,来解决组件的懒加载问题。

Vue组件懒加载方案介绍

项目Github地址: github.com/xunleif2e/v…

这个是我们基于上面的思考做的一个通用的解决方案,下面简单介绍一下特性、使用以及 API 方面的知识,后面结合 5 个具体的 DEMO 来讲解更高级的用法。

特性

  • 支持 组件可见或即将可见时懒加载
  • 支持 组件延时加载
  • 支持 加载组件前展示组件骨架,提高用户体验
  • 支持 懒加载组件分包异步加载

安装和使用

npm i @xunlei/vue-lazy-component
  • 方式1 利用插件方式全局注册
  • 方式2 局部注册
  • 方式3 独立版本引入,自动全局注册

用法

使用方式
使用方式

Props

参数 说明 类型 可选值 默认值
viewport 组件所在的视口,如果组件是在页面容器内滚动,视口就是该容器 HTMLElement true null,代表视窗
direction 视口的滚动方向, vertical代表垂直方向,horizontal代表水平方向 String true vertical
threshold 预加载阈值, css单位 String true 0px
tagName 包裹组件的外层容器的标签名 String true div
timeout 等待时间,如果指定了时间,不论可见与否,在指定时间之后自动加载 Number true -

Events

事件名 说明 事件参数
before-init 模块可见或延时截止导致准备开始加载懒加载模块 -
init 开始加载懒加载模块,此时骨架组件开始消失 -
before-enter 懒加载模块开始进入 el
before-leave 骨架组件开始离开 el
after-leave 骨架组件已经离开 el
after-enter 懒加载模快已经进入 el
after-init 初始化完成 -

DEMO 1 超长页面懒加载

xunleif2e.github.io/vue-lazy-co…

<vue-lazy-component>
    <st-series-sohu/>
    <st-series-sohu-skeleton slot="skeleton"/>
</vue-lazy-component>

通过上面这种简单的使用方式就可以实现组件即将可见时自动加载。

DEMO 2 延时加载

xunleif2e.github.io/vue-lazy-co…

延时加载
延时加载

<vue-lazy-component :timeout="1000">
    <st-series-sohu/>
    <st-series-sohu-skeleton slot="skeleton"/>
</vue-lazy-component>
`

如果有时候仅仅是希望某些组件稍后渲染,而不一定要等到可见时,可以通过这种方式。

比如我们业务中可能会有些运营性质的挂件,就可以采取延时加载的方式。

DEMO 3 自定义过渡效果

xunleif2e.github.io/vue-lazy-co…

自定义过渡效果
自定义过渡效果

如果觉得 Vue Lazy Component 自带的淡入淡出的过渡效果太丑,或者需要调整淡入淡出效果的时长,就可以通过自定义样式来改变过渡效果。这个例子演示了另外一种过渡效果,transition 的生命周期可以参考 Vue.js 的 transition 组件的文档。

DEMO 4 webpack 分包

xunleif2e.github.io/vue-lazy-co…

懒加载组件异步加载
懒加载组件异步加载

DEMO1 演示了如何懒加载模块,但其实只是推迟了模块的渲染和模块内的资源的加载,如果我们需要更进一步,连模块本身的代码也是懒加载,就像 AMD 那样异步按需加载,这个也是可以做到的。

这里可以利用Vue.js的异步组件,将每个真实组件都注册成异步组件,在异步组件的工厂函数里使用 Webpack 的 AMD 版本的 require ,就可以实现真实组件可以分成独立的 bundle 加载,脱离页面 js 的bundle。

但是这里会有个问题,就算模块是可见时才渲染,在打开页面的时候会发现模块不可见之前它的 bundle 已经加载了,这并没有实现按需加载。

这个例子演示了一种做法,Vue Lazy Component 可以在即将切换真实组件前通过 Scoped Slots 传递一个 loading 属性给真实组件,真实组件只要是根据这个 loading 来条件渲染就可以避免非按需加载,这个和 Vue.js 对组件的解析机制有关,例子里有相应的的代码,有兴趣的同学可以深入研究下。

DEMO 5 特定视口内懒加载

xunleif2e.github.io/vue-lazy-co…

IM场景
IM场景

在某些场景下,我们要解决滚动容器内的组件懒加载,这个时候可见性是相对与这个视口来的,这个例子演示了如何指定聊天窗口作为观察的视口。

这里吐槽下Vue.js的 $parent 、$refs 的设计,它们都不是响应式的,如果需要动态获取这些组件引用上的 $el ,必须要等到 mounted 事件发生之后,所以例子的代码稍微有一点繁琐。

应用效果

首先 Vue Lazy Component 的设计虽然是说组件级的,其实它的粒度可大可小,大的比如页面不同的区域,小的就像 DEMO5 里的只是一个用户头像,所以适用性非常强,只要有懒加载需求的场景基本都可以采用。

另外,在终端方面,不仅可以兼容PC端的项目,在移动端也是可以使用的,当然,需要解决 IntersectionObserver API 的兼容性问题,在项目 Readme 里提到了 w3c 的 polyfill 的地址。

应用业务

我们目前应用在迅雷的两个项目中,一个是 PC 迅雷的首页项目,一个是 PC 迅雷的组队加速项目,后期预计会推广到更多的业务中去。

迅雷前端项目
迅雷前端项目

优化后请求瀑布图

我们再来看看开始那个页面的情况,在使用了 组件懒加载技术后,请求数变成了只有 31 个,瀑布图变得比较短了。

请求数变为31个
请求数变为31个

数据对比

我们把前后的数据进行一个对比:

  • 请求数变成之前的 1 / 5,优化效果比较明显
  • 请求大小相比之前降低不太明显
  • load 时长也同样不太明显

分析主要是有较多图片未按照使用尺寸裁剪和压缩,导致请求大小较大,同时造成了 load 时长的拖长。

后续性能优化方向

后续我们会继续来优化这个页面,主要的方向有两个:

图片尺寸适配和压缩

通过图片的裁剪和压缩,解决请求资源大小较大子资源加载时间较长导致 load 时间拖长的问题

预渲染

采用预渲染插件将页面的主要 css 、js 进行内联,将骨架架屏通过预渲染生成出来,这样可以避免 SPA 首屏可见关键路径较长的问题,在页面解析完 dom 树以后即可保证首屏可见。

懒加载方案 ROADMAP

Vue Lazy Component 懒加载方案还有些地方做得还不够好,计划在后期的几个小版本里支持以下的特性:

  • SSR 支持 v1.1.0
  • UI单元测试 v1.2.0
  • 减少性能开销 v1.3.0
    • 重绘
    • FPS

后记

这篇文章分享了从遇到业务实际性能问题,到分析、解决并梳理出通用的解决方案的过程,重点其实不是最终的实现代码实现,而是解决问题的角度和过程。

最后欢迎大家通过提交 issue 或者 PR 的方式参与贡献,项目 Github 地址: github.com/xunleif2e/v…


扫一扫关注迅雷前端公众号