IntersectionObserver结合lazyload的首页csr模式下多模块加载优化方案

前言

笔者最近接连收到好几个做多模块官网的需求,这类需求一般是做一个长内容的pc官网,外加响应式能适配移动端即可。这类需求一般都有一下特性:

1.多模块 2.内容为主 3.图文视频直播元素丰富 4.长度高的几千像素,长滚动

这种元素 布局 丰富的官网固然非常美观,但是性能因素却令人担忧,因为如果不加处理,首页加载速度一定会非常慢,就像早高峰的南光高速一样。

因为直播元素使用了腾讯播放器sdk,在gzip模式下也能大到210kb,算上字体文件,如果一股脑加载的话,整个官网首页的大小可能得达到几M,完整加载可能要到数秒以上的时间。

接下来我会展示怎么处理这类性能问题,让整个项目实现秒开。

拆分模块,实现lazyload

as we all know,如果我们不加拆分模块,那么所有的内容都会被打包进一个js,这个js就会包含所有的内容。所以我们第一步,把首页的每一个模块单独抽离成为组件。

第二步,我们使用React的能力,将每个模块作为dynamic module import

截止到目前为止,我们已经初步完成了lazyload的工作,接下来,我们要做的工作就是如何,我们在pc浏览器把首页打开,只加载banner相关的,其他所有模块的东西不加载,这时候就要请出IntersectionObserver

IntersectionObserver

不知道读者们有没有经历过这样的需求,当你想要监控视窗内的元素在不在可视区,或者想知道元素在可视区的哪个位置,如果用常规js来写,兼容问题会让你头疼,于是这种需求被浏览器原生实现了。

于是乎,我就想到一种方案,给每一个模块提前生成一个占位元素,这样能撑起滚动,再结合IntersectionObserver,当滚动到对应的占位元素时,我再把真正的内容模块替换掉占位元素进行渲染。

代码实现如下,我先对IntersectionObserver进行了hook 封装

ts 复制代码
import { useEffect, useRef } from "react";

export default function useIntersectionObserver(eleKey: string, handlers = {} as any) {
    const { onShow = Function.prototype, onHide = Function.prototype, ifInit = true } = handlers;
    const obj = useRef(null as any);

    useEffect(() => {

        if (!ifInit) {
            return;
        }
        const observer = obj.current = new IntersectionObserver(changes => {

            for (const change of changes) {

                if (change.isIntersecting) {
                    onShow();
                } else {
                    onHide();
                }
            }
        }, {});
        observer.observe(document.querySelector(eleKey)!);


        return () => {
            obj.current && obj.current.disconnect();
        }
    }, [ifInit]);
}

再后来,我需要专门设计一个组件,既能产生一个占位元素,又能使用useIntersectionObserver进行模块替换

tsx 复制代码
import useIntersectionObserver from "@/hooks/useElementObserver";
import { useEffect, useMemo, useState } from "react";
import "./index.less";
import eventEmitter from "@/helpers/event";

/**
 * 懒加载模块
 */
export default function LazyModule(props) {
    const [importModule, setImportModule] = useState(false);
    const [module, setModule] = useState(null as any);

    const eleId = useMemo(() => `lazy${Date.now().toString(16)}-${props.moduleName}`, [props.moduleName]);

    useIntersectionObserver(`#${eleId}`, {
        onShow: () => {
            console.log(`%celement of ${eleId} is show`, 'color: #43bb88;font-size: 12px;');
            setImportModule(true)
        },
    });

    const loadModule = async () => {
        const Module = props.module;

        if (module) return;

        setModule(<Module />)
    }
    useEffect(() => {
        importModule && loadModule();
    }, [importModule]);

    useEffect(() => {
        const eventLoad = () => {
            setImportModule(i => !i ? true : i);
        };

        props.moduleName && eventEmitter.on(`lazy-load`, eventLoad);

        return () => {
            eventEmitter.off(`lazy-load`, eventLoad);
        }
    }, []);

    if (module) return module;

    return <div id={eleId} className="lazy-module">
        <svg className="placeImg" viewBox="0 0 1024 3024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5013" width="750" height="1440"><path d="M928 896H96a53.393333 53.393333 0 0 1-53.333333-53.333333V181.333333a53.393333 53.393333 0 0 1 53.333333-53.333333h832a53.393333 53.393333 0 0 1 53.333333 53.333333v661.333334a53.393333 53.393333 0 0 1-53.333333 53.333333zM96 170.666667a10.666667 10.666667 0 0 0-10.666667 10.666666v661.333334a10.666667 10.666667 0 0 0 10.666667 10.666666h832a10.666667 10.666667 0 0 0 10.666667-10.666666V181.333333a10.666667 10.666667 0 0 0-10.666667-10.666666z" fill="#5C5C66" p-id="5014"></path></svg>
    </div>
}

原理非常简单,一个简单的条件渲染,而我采用的占位元素只是一个简单的svg,因为svg不占请求资源,浏览器渲染压力也非常小。

最后的布局如下

效果

页面打开,只加载了必须的react/lodash/vendor等cdn资源,不加载模块等业务资源

当我滚动的时候,资源随着滚动的加载情况

浏览器渲染情况 FP总体在1秒以内,实现秒开,各项任务也是绿色的 没有明显的阻塞

红框内的资源都是滚动的时候时序加载的显示

以此实现了多模块/元素/资源的页面打开的最优解

相关推荐
hackeroink4 分钟前
【2024版】最新推荐好用的XSS漏洞扫描利用工具_xss扫描工具
前端·xss
迷雾漫步者2 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人6 小时前
前端知识补充—CSS
前端·css