Vue首屏性能优化组件
Vue首屏性能优化组件
简单实现一个Vue首屏性能优化组件,现代化浏览器提供了很多新接口,在不考虑IE兼容性的情况下,这些接口可以很大程度上减少编写代码的工作量以及做一些性能优化方面的事情,当然为了考虑IE我们也可以在封装组件的时候为其兜底,本文的首屏性能优化组件主要是使用IntersectionObserver以及requestIdleCallback两个接口。
描述
先考虑首屏场景,当做一个主要为展示用的首屏时,通常会加载较多的资源例如图片等,如果我们不想在用户打开时就加载所有资源,而是希望用户滚动到相关位置时再加载组件,此时就可以选择IntersectionObserver这个接口,当然也可以使用onscroll事件去做一个监听,只不过这样性能可能比较差一些。还有一些组件,我们希望他必须要加载,但是又不希望他在初始化页面时同步加载,这样我们可以使用异步的方式比如Promise和setTimeout等,但是如果想再降低这个组件加载的优先级,我们就可以考虑requestIdleCallback这个接口,相关代码在https://github.com/WindrunnerMax/webpack-simple-environment的vue--first-screen-optimization分支。
IntersectionObserver
IntersectionObserver接口,从属于Intersection Observer API,提供了一种异步观察目标元素与其祖先元素或顶级文档视窗viewport交叉状态的方法,祖先元素与视窗viewport被称为根root,也就是说IntersectionObserver API,可以自动观察元素是否可见,由于可见visible的本质是,目标元素与视口产生一个交叉区,所以这个API叫做交叉观察器,兼容性https://caniuse.com/?search=IntersectionObserver。
const io = new IntersectionObserver(callback, option);
// 开始观察
io.observe(document.getElementById("example"));
// 停止观察
io.unobserve(element);
// 关闭观察器
io.disconnect();
- 参数
callback,创建一个新的IntersectionObserver对象后,当其监听到目标元素的可见部分穿过了一个或多个阈thresholds时,会执行指定的回调函数。 - 参数
option,IntersectionObserver构造函数的第二个参数是一个配置对象,其可以设置以下属性:threshold属性决定了什么时候触发回调函数,它是一个数组,每个成员都是一个门槛值,默认为[0],即交叉比例intersectionRatio达到0时触发回调函数,用户可以自定义这个数组,比如[0, 0.25, 0.5, 0.75, 1]就表示当目标元素0%、25%、50%、75%、100%可见时,会触发回调函数。root属性指定了目标元素所在的容器节点即根元素,目标元素不仅会随着窗口滚动,还会在容器里面滚动,比如在iframe窗口里滚动,这样就需要设置root属性,注意,容器元素必须是目标元素的祖先节点。rootMargin属性定义根元素的margin,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小,它使用CSS的定义方法,比如10px 20px 30px 40px,表示top、right、bottom和left四个方向的值。
- 属性
IntersectionObserver.root只读,所监听对象的具体祖先元素element,如果未传入值或值为null,则默认使用顶级文档的视窗。 - 属性
IntersectionObserver.rootMargin只读,计算交叉时添加到根root边界盒bounding box的矩形偏移量,可以有效的缩小或扩大根的判定范围从而满足计算需要,此属性返回的值可能与调用构造函数时指定的值不同,因此可能需要更改该值,以匹配内部要求,所有的偏移量均可用像素pixel、px或百分比percentage、%来表达,默认值为0px 0px 0px 0px。 - 属性
IntersectionObserver.thresholds只读,一个包含阈值的列表,按升序排列,列表中的每个阈值都是监听对象的交叉区域与边界区域的比率,当监听对象的任何阈值被越过时,都会生成一个通知Notification,如果构造器未传入值,则默认值为0。 - 方法
IntersectionObserver.disconnect(),使IntersectionObserver对象停止监听工作。 - 方法
IntersectionObserver.observe(),使IntersectionObserver开始监听一个目标元素。 - 方法
IntersectionObserver.takeRecords(),返回所有观察目标的IntersectionObserverEntry对象数组。 - 方法
IntersectionObserver.unobserve(),使IntersectionObserver停止监听特定目标元素。
此外当执行callback函数时,会传递一个IntersectionObserverEntry对象参数,其提供的信息如下。
time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒。target:被观察的目标元素,是一个DOM节点对象。rootBounds:根元素的矩形区域的信息,是getBoundingClientRect方法的返回值,如果没有根元素即直接相对于视口滚动,则返回null。boundingClientRect:目标元素的矩形区域的信息。intersectionRect:目标元素与视口或根元素的交叉区域的信息。intersectionRatio:目标元素的可见比例,即intersectionRect占boundingClientRect的比例,完全可见时为1,完全不可见时小于等于0。
{
time: 3893.92,
rootBounds: ClientRect {
bottom: 920,
height: 1024,
left: 0,
right: 1024,
top: 0,
width: 920
},
boundingClientRect: ClientRect {
// ...
},
intersectionRect: ClientRect {
// ...
},
intersectionRatio: 0.54,
target: element
}
requestIdleCallback
requestIdleCallback方法能够接受一个函数,这个函数将在浏览器空闲时期被调用,这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应,函数一般会按先进先调用的顺序执行,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序,兼容性https://caniuse.com/?search=requestIdleCallback。
const handle = window.requestIdleCallback(callback[, options]);
requestIdleCallback方法返回一个ID,可以把它传入window.cancelIdleCallback()方法来结束回调。- 参数
callback,一个在事件循环空闲时即将被调用的函数的引用,函数会接收到一个名为IdleDeadline的参数,这个参数可以获取当前空闲时间以及回调是否在超时时间前已经执行的状态。 - 参数
options可选,包括可选的配置参数,具有如下属性:timeout: 如果指定了timeout,并且有一个正值,而回调在timeout毫秒过后还没有被调用,那么回调任务将放入事件循环中排队,即使这样做有可能对性能产生负面影响。
实现
实际上编写组件主要是搞清楚如何使用这两个主要的API就好,首先关注IntersectionObserver,因为考虑需要使用动态组件<component />,那么我们向其传值的时候就需要使用异步加载组件() => import("component")的形式。监听的时候,可以考虑加载完成之后即销毁监听器,或者离开视觉区域后就将其销毁等,这方面主要是策略问题。在页面销毁的时候就必须将Intersection Observer进行disconnect,防止内存泄漏。使用requestIdleCallback就比较简单了,只需要将回调函数执行即可,同样也类似于Promise.resolve().then这种异步处理的情况。
这里是简单的实现逻辑,通常observer的使用方案是先使用一个div等先进行占位,然后在observer监控其占位的容器,当容器在视区时加载相关的组件,相关的代码在https://github.com/WindrunnerMax/webpack-simple-environment的vue--first-screen-optimization分支,请尽量使用yarn进行安装,可以使用yarn.lock文件锁住版本,避免依赖问题。使用npm run dev运行之后可以在Console中看到这四个懒加载组件created创建的顺序,其中A的observer懒加载是需要等其加载页面渲染完成之后,判断在可视区,才进行加载,首屏使能够直接看到的,而D的懒加载则是需要将滚动条滑动到D的外部容器出现在视图之后才会出现,也就是说只要不滚动到底部是不会加载D组件的,另外还可以通过component-params和component-events将attrs和listeners传递到懒加载的组件,类似于$attrs和$listeners,至此懒加载组件已简单实现。
<!-- App.vue -->
<template>
<div>
<section>1</section>
<section>
<div>2</div>
<lazy-load
:lazy-component="Example"
type="observer"
:component-params="{ content: 'Example A' }"
:component-events="{
'test-event': testEvent,
}"
></lazy-load>
</section>
<section>
<div>3</div>
<lazy-load
:lazy-component="Example"
type="idle"
:component-params="{ content: 'Example B' }"
:component-events="{
'test-event': testEvent,
}"
></lazy-load>
</section>
<section>
<div>4</div>
<lazy-load
:lazy-component="Example"
type="lazy"
:component-params="{ content: 'Example C' }"
:component-events="{
'test-event': testEvent,
}"
></lazy-load>
</section>
<section>
<div>5</div>
<lazy-load
:lazy-component="Example"
type="observer"
:component-params="{ content: 'Example D' }"
:component-events="{
'test-event': testEvent,
}"
></lazy-load>
</section>
</div>
</template>
<script lang="ts">
import { Component, Vue } from "vue-property-decorator";
import LazyLoad from "./components/lazy-load/lazy-load.vue";
@Component({
components: { LazyLoad },
})
export default class App extends Vue {
protected Example = () => import("./components/example/example.vue");
protected testEvent(content: string) {
console.log(content);
}
}
</script>
<style lang="scss">
@import "./common/styles.scss";
body {
padding: 0;
margin: 0;
}
section {
margin: 20px 0;
color: #fff;
height: 500px;
background: $color-blue;
}
</style>
<!-- lazy-load.vue -->
<template>
<div>
<component
:is="renderComponent"
v-bind="componentParams"
v-on="componentEvents"
></component>
</div>
</template>
<script lang="ts">
import { Component, Prop, Vue } from "vue-property-decorator";
@Component
export default class LazyLoad extends Vue {
@Prop({ type: Function, required: true })
lazyComponent!: () => Vue;
@Prop({ type: String, required: true })
type!: "observer" | "idle" | "lazy";
@Prop({ type: Object, default: () => ({}) })
componentParams!: Record<string, unknown>;
@Prop({ type: Object, default: () => ({}) })
componentEvents!: Record<string, unknown>;
protected observer: IntersectionObserver | null = null;
protected renderComponent: (() => Vue) | null = null;
protected mounted() {
this.init();
}
private init() {
if (this.type === "observer") {
// 存在`window.IntersectionObserver`
if (window.IntersectionObserver) {
this.observer = new IntersectionObserver(entries => {
entries.forEach(item => {
// `intersectionRatio`为目标元素的可见比例,大于`0`代表可见
// 在这里也有实现策略问题 例如加载后不解除`observe`而在不可见时销毁等
if (item.intersectionRatio > 0) {
this.loadComponent();
// 加载完成后将其解除`observe`
this.observer?.unobserve(item.target);
}
});
});
this.observer.observe(this.$el.parentElement || this.$el);
} else {
// 直接加载
this.loadComponent();
}
} else if (this.type === "idle") {
// 存在`requestIdleCallback`
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
if (window.requestIdleCallback) {
requestIdleCallback(this.loadComponent, { timeout: 3 });
} else {
// 直接加载
this.loadComponent();
}
} else if (this.type === "lazy") {
// 存在`Promise`
if (window.Promise) {
Promise.resolve().then(this.loadComponent);
} else {
// 降级使用`setTimeout`
setTimeout(this.loadComponent);
}
} else {
throw new Error(`type: "observer" | "idle" | "lazy"`);
}
}
private loadComponent() {
this.renderComponent = this.lazyComponent;
this.$emit("loaded");
}
protected destroyed() {
this.observer && this.observer.disconnect();
}
}
</script>
每日一题
https://github.com/WindrunnerMax/EveryDay
参考
https://www.ruanyifeng.com/blog/2016/11/intersectionobserver_api.html
https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
Vue首屏性能优化组件的更多相关文章
- 【Vuejs】317- 提升90%加载速度——Vuecli下的首屏性能优化
点击上方"前端自习课"关注,学习起来~,所以接下来还会介绍一些它们在优化上的异同 的话,先安装插件 cnpm intall webpack-bundle-analyzer –sav ...
- 【Vuejs】269- 提升90%加载速度——vuecli下的首屏性能优化
前言 之前用 ,所以接下来还会介绍一些它们在优化上的异同 分析 vuecli 2.x自带了分析工具只要运行 npm run build --report 如果是 vuecli 3的话,先安装插件 cn ...
- react 首屏性能优化
首屏优化点:1.加载包(bundle.js)文件的大小,越小,首屏渲染速度越快 (按需加载) 2.优先渲染用户直观看到的页面部分(懒加载) 技术点:react-loadable . react-laz ...
- vue 首屏渲染优化 -- 这个不错
这篇文章分享了从遇到前端业务性能问题,到分析.解决并且梳理出通用的Vue 2.x 组件级懒加载解决方案(Vue Lazy Component )的过程. 初始加载资源过多 问题起源于我们的一个页面,下 ...
- 移动 H5 首屏秒开优化方案探讨
转载bang大神文章,原文<移动 H5 首屏秒开优化方案探讨>,此文仅仅用做自学与分享! 随着移动设备性能不断增强,web 页面的性能体验逐渐变得可以接受,又因为 web 开发模式的诸多好 ...
- vue首屏加载优化
库使用情况 vue vue-router axios muse-ui material-icons vue-baidu-map 未优化前 首先我们在正常情况下build 优化 1. 按需加载 当前流行 ...
- 关于VUE首屏加载过长的优化,VUE项目开发优化
1. 按需引入UI组件 当下市面上流行的UI组件基本都支持按需加载,本文以Element UI为例: (1) 新建一个elementUI.js文件 (2) 访问地址找到按需引入方式的说 ...
- Vue-cli3性能优化
Vue-cli3.0的打包性能优化方案:https://juejin.im/post/5d42962be51d4561b84c00c3 提升90%加载速度——vuecli下的首屏性能优化:https: ...
- Vue优化首屏加载
背景: 使用vue + iview搭建的一个后台管理系统,路由已经用了懒加载,加载登陆页面,居然还是需要18S左右,刚到一个新公司,项目经理很委婉的说,看看能不能优化了一下.然后就开始了网上一大堆'v ...
随机推荐
- spring入门1-IOC和DI
1.概述 1.1.简介 Spring是分层的 Java SE/EE应用 full-stack 轻量级开源框架,以 IoC(Inverse Of Control:反转控制)和 AOP(Aspect Or ...
- phpstorm 配置Psr4 风格代码
http://www.cnblogs.com/xp796/p/6441700.html
- python继承细节
不要子类化内置类型 内置类型(由C语言编写)不会调用用户定义的类覆盖的特殊方法. 例如,子类化dict作为测验: class DoppeDict(dict): def __setitem__(self ...
- eval(input())
看到一段代码,判读输入的数字,用的是eval(input()),查了一下,原来input()会把所有输入值,包括数字,视为字符串,而eval()会去掉字符串最外层的引号,然后当做Python语句执行[ ...
- 鸿蒙内核源码分析(内存汇编篇) | 谁是虚拟内存实现的基础 | 百篇博客分析OpenHarmony源码 | v14.14
百篇博客系列篇.本篇为: v14.xx 鸿蒙内核源码分析(内存汇编篇) | 谁是虚拟内存实现的基础 | 51.c.h .o 内存管理相关篇为: v11.xx 鸿蒙内核源码分析(内存分配篇) | 内存有 ...
- P4320-道路相遇,P5058-[ZJOI2004]嗅探器【圆方树,LCA】
两题差不多就一起写了 P4320-道路相遇 题目链接:https://www.luogu.com.cn/problem/P4320 题目大意 \(n\)个点\(m\)条边的一张图,\(q\)次询问两个 ...
- GoLang设计模式08 - 命令模式
命令模式是一种行为型模式.它建议将请求封装为一个独立的对象.在这个对象里包含请求相关的全部信息,因此可以将其独立执行. 在命令模式中有如下基础组件: Receiver:唯一包含业务逻辑的类,命令对象会 ...
- Linux虚拟机配置静态ip地址
使用VMware搭建的虚拟机ip地址经常变动,在这里记录一下虚拟机设置静态ip地址: 首先通过VMware菜单栏编辑->虚拟网络编辑器->NAT设置查看子网ip地址和网关ip: 例如我这里 ...
- java语言程序设计与数据结构(基础篇)第二章答案
答案为本人自己求解,若有错误,还望海涵并及时告知.如有雷同,纯属巧合. 2.1 import java.util.Scanner; public class Welcome { public stat ...
- 题解 [JSOI2011]柠檬
题目传送门 题目大意 给出一个区间,每个点都有一个颜色,把这个区间分为许多块,每一块的权值为 \(\max\{s\times t^2\}\) ,其中 \(s\) 为某种颜色,\(t\) 为该颜色在该块 ...