时间:2022-05-24 09:35:35 | 栏目:vue | 点击:次
简单实现一个Vue首屏性能优化组件,现代化浏览器提供了很多新接口,在不考虑IE兼容性的情况下,这些接口可以很大程度上减少编写代码的工作量以及做一些性能优化方面的事情,当然为了考虑IE我们也可以在封装组件的时候为其兜底,本文的首屏性能优化组件主要是使用IntersectionObserver以及requestIdleCallback两个接口。
先考虑首屏场景,当做一个主要为展示用的首屏时,通常会加载较多的资源例如图片等,如果我们不想在用户打开时就加载所有资源,而是希望用户滚动到相关位置时再加载组件,此时就可以选择IntersectionObserver这个接口,当然也可以使用onscroll事件去做一个监听,只不过这样性能可能比较差一些。还有一些组件,我们希望他必须要加载,但是又不希望他在初始化页面时同步加载,这样我们可以使用异步的方式比如Promise和setTimeout等,但是如果想再降低这个组件加载的优先级,我们就可以考虑requestIdleCallback这个接口,相关代码在https://github.com/WindrunnerMax/webpack-simple-environment的vue--first-screen-optimization分支。
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函数时,会传递一个IntersectionObserverEntry对象参数,其提供的信息如下。
{ 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方法能够接受一个函数,这个函数将在浏览器空闲时期被调用,这使开发者能够在主事件循环上执行后台和低优先级工作,而不会影响延迟关键事件,如动画和输入响应,函数一般会按先进先调用的顺序执行,如果回调函数指定了执行超时时间timeout,则有可能为了在超时前执行函数而打乱执行顺序,兼容性https://caniuse.com/?search=requestIdleCallback。
const handle = window.requestIdleCallback(callback[, options]);
实际上编写组件主要是搞清楚如何使用这两个主要的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> Copy <!-- 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