我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值。
本文作者:霁明
什么是懒加载(lazy loading)
懒加载是一种将资源标识为非阻塞(非关键)资源并仅在需要时加载它们的策略。这是一种缩短关键渲染路径长度的方法,可以缩短页面加载时间。
懒加载可以在应用程序的不同时刻发生,但通常会在某些用户交互(例如滚动和导航)上发生。
图片懒加载
为什么要对图片懒加载
1、提升性能 减少对图片资源的请求,减少了对网络资源的占用,使得加载其他资源变得更快。与没有使用图片懒加载的页面相比,页面会更快。(图片数量越多、体积越大差异越明显) 2、降低成本 网络资源一般根据传输字节数收费,减少对图片资源的请求,减少了传输字节数,从而降低成本。
实现图片懒加载方式
1、img 标签的 loading 属性
tsx
<img src="image.png" loading="lazy" width={300} height={200} />
loading 属性有两个值:
eager 立即加载图像,不管它是否在视口之外(默认值);
lazy 延迟加载图像,直到它和视口接近到一个计算得到的距离(由浏览器定义,chrome、firfox、edge、safari 都是出现则立即加载)。注意,img 元素需要指定宽高 lazy 才能生效。
兼容性
兼容性在整体还可以,但如果要兼容低版本就不行了。
优点:使用简单,兼容性也还可以。
缺点:图片加载完成前,会看到空白区域,用户体验不是很好。
2、JS事件监听
监听 scroll、resize 等事件,当事件触发时,获取图片元素的位置信息、滚动高度及视口高度,计算出当前图片元素是否出现在视口内,如果出现了则加载图片。
tsx
useEffect(() => {
const container = ref.current;
const lazyLoad = () => {
if (!container) return;
let timer = null;
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => {
const imgElements =
container?.querySelectorAll<HTMLImageElement>('.lazy');
imgElements?.forEach((img) => {
if (
img.offsetTop <
window.innerHeight + container.scrollTop
) {
img.src = img.dataset.src || '';
img.classList.remove('lazy');
}
});
if (!imgElements?.length) {
container.removeEventListener('scroll', lazyLoad);
}
}, 100);
};
lazyLoad();
container?.addEventListener('scroll', lazyLoad);
return () => {
container?.removeEventListener('scroll', lazyLoad);
};
}, []);
以上示例通过 offsetTop、scrollTop、innerHeight 来判断图片是否在视口内,另外也可以通过 getBoundingClientRect 方法来获取元素的位置信息,进而进行判断,原理类似。
优点:所有浏览器都兼容,可以比较灵活,自定义触发加载的时机,可以结合图片加载预览提升用户体验。
缺点:需要通过事件触发,图片的位置需要手动计算,容易导致性能问题。
3、Intersection Observer API
交叉观察器 API(Intersection Observer API)提供了一种异步检测目标元素与祖先元素或顶级文档的视口相交情况变化的方法。
补充:目标元素位置是通过 getBoundingClientRect() 方法确定的,所以无论是图片在正常文档流中还是脱离文档流,或者使用了 transform 改变渲染位置,最终都会根据元素相对于视口的实际渲染位置进行处理(可以理解为和视觉上是统一的,看到了就会触发)。
通过这个API,可以检测元素是否进入视口,并在元素进入视口时进行相应的处理。以下是简单使用示例:
tsx
useEffect(() => {
const container = ref.current;
const imgElements =
container?.querySelectorAll<HTMLImageElement>('.lazy');
const imgObserver = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement;
img.src = img.dataset.src || '';
img.classList.remove('lazy');
imgObserver.unobserve(img);
}
});
});
imgElements?.forEach(function (img) {
imgObserver.observe(img);
});
}, []);
创建观察器后,使用观察器对所有懒加载 img 元素进行观察,创建观察器时传入的回调函数会在每个 img 元素进入视口时调用。
回调函数的 entries 参数是状态发生变化的元素对象,entry 对象的 isIntersecting 为 true 则说明元素进入了视口。
上面代码中,当元素进入视口后,修改 img 元素的 src 来加载图片,并取消对当前元素的观察。
兼容性
兼容 chrome 版本 >= 58,兼容性整体还可以。
优点:使用简单,不依赖事件监听,也不需要手动计算元素位置,性能更好。
缺点:不兼容旧的浏览器版本(例如chrome < 58)。
页面懒加载
目前数栈产品中实现
目前数栈产品中使用 React.lazy + Suspense + ReactRouter 实现页面懒加载,示例代码如下:
tsx
const createLazyRoute = (RouteComponent: any) => {
return function (props: any) {
return (
<Suspense fallback={<Loading />}>
<RouteComponent {...props} />
</Suspense>
);
};
};
const DataSourceList = createLazyRoute(lazy(() => import('@/pages/dataSource/list')));
const routers = createHashRouter([
...
{
path: 'data-source',
Component: PubManageLayout,
children: [
{
path: 'list',
Component: DataSourceList,
},
],
},
...
]);
页面懒加载流程及原理:
tips:关于 React.lazy 和 Suspense 的源码实现原理,可以查看分享《React18之Suspense》
目前这种方案实现了页面懒加载,提升了首屏加载速度,但是有一个明显的缺点,就是当第一次访问懒加载的页面时,经常需要先 loading 一段时间,普通页面还好加载时间较短,部分页面代码较多或使用到一些体积较大的第三方库时,加载时间就比较长,用户体验不是很友好。
产品中实践的优化方案
优化方案
使用预加载来优化页面懒加载等待问题,对于页面上的跳转链接或某些特定元素,当被鼠标hover时,提前加载需要跳转的页面,当用户点击时,可以快速打开对应的页面。 优化后页面加载流程如下:
具体实现
结合当前实现方式,进行调整:在 createLazyRoute 方法中,将 factory 挂载到 preload 属性上
tsx
const createLazyRoute = (factory: () => Promise<{ default: React.ComponentType<any> }>) => {
const RouteComponent = lazy(factory);
const LazyRoute = function (props: any) {
return (
<Suspense fallback={<LazyLoading />}>
<RouteComponent {...props} />
</Suspense>
);
};
LazyRoute.preload = factory;
return LazyRoute;
};
封装一个 Preload 组件,给被包裹的元素传递一个 onMouseEnter 属性(被包裹元素须支持该属性),当鼠标 hover 时,触发 onMouseEnter 方法。首先会根据要跳转的 pathname 和 routers,找到对应路由组件,然后执行路由组件上的 preload 方法,相当于直接执行 import('..') 进行动态导入,即会立即加载对应的页面,从而实现预加载。
tsx
function Preload({ children, pathname }: IPreload) {
const findRouteComponent = (path, routes) => {
try {
const url = new URL(path, location.origin);
const matchs = matchRoutes(routes, { pathname: url.pathname, search: url.search });
let matchedComponent = null;
matchs?.forEach((item: any) => {
if (
matchPath(item.pathname, path) &&
item.route?.path !== '/*' &&
item.route?.Component
) {
matchedComponent = item.route.Component;
}
});
return matchedComponent;
} catch (error) {
console.error(error);
return null;
}
};
const preloadRouteComponent = (path) => {
const component = findRouteComponent(path, routes);
if (typeof component === 'function') {
const preload = component.preload || component()?.type?.preload;
preload?.();
}
};
const onMouseEnter = () => {
preloadRouteComponent(pathname);
};
return React.cloneElement(children, { onMouseEnter });
}
Preload 组件使用示例:
tsx
<Preload pathname="/project/create">
<Button onClick={this.createProjectSpace}>
创建
</Button>
</Preload>
小结
本文针对懒加载主要的两个场景图片懒加载、页面懒加载进行介绍,介绍了图片的三种懒加载方式,以及产品中的做的页面懒加载优化方案,有问题欢迎一起讨论。
最后
欢迎关注【袋鼠云数栈UED团队】~
袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star