前言
笔者最近接连收到好几个做多模块官网的需求,这类需求一般是做一个长内容的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秒以内,实现秒开,各项任务也是绿色的 没有明显的阻塞
红框内的资源都是滚动的时候时序加载的显示
以此实现了多模块/元素/资源的页面打开的最优解