1 什么是懒加载
懒加载(lazyload)是一种将资源标识为非阻塞(非关键)资源并仅在需要时加载它们的策略。对于前端来说懒加载是对网页性能优化的一种方案,其核心是延迟加载/按需加载。
需要时是指?
比如我们加载一个页面,这个页面很长很长,已经远远超出我们的浏览器可视区域,那么懒加载可以是优先加载可视区域的内容,其他部分等进入了可视区域再加载。
2 懒加载的重要性/解决了什么问题?
面对急剧增加的资源数量和规模,及spa单页面应用越来越广泛的背景下,懒加载的意义体现在:
:::tips
- 提升用户体验:如果同时加载较多的图片,可能需要等待的时间较长,这样影响用户的体验,而使用懒加载就能提高用户的体验,其次可以提高页面的首屏渲染速度
- 减少无用的资源加载:使用懒加载减轻服务器压力,同时也减少了浏览器的负担
- 防止加载过多图片而影响其他资源的加载:会影响网站应用的正常使用
:::
3 懒加载策略
懒加载可以通过多种策略应用于多种资源,以及不同的场景。
:::tips
- 常规的代码拆分
- JavaScript 模块
- css
- 图片和视频
- ……
:::
3.1 图片懒加载
图片资源应用广泛,且带宽消耗大,一个应用存在大量图片会影响加载速度和用户体验。图片懒加载是一种优化网页性能的方式,大量用在电商场景中。
uland.taobao.com/sem/tbsearc…
www.jd.com/
原理:
不同电商网站虽然实现懒加载的方式略有差异,但原理基本一致:主要是监听scroll_ _事件,当用户滚动页面时,判断元素是否处于可视区域,可视区域外的元素资源(如图片)不会加载。
实现思路:
我们实现懒加载的想法是一开始只加载显示在视口内的图片,从上图可以看出,我们可以先只加载前两张图片。那么如何保证图片不加载出来呢?
我们知道,具备src属性时,浏览器才会请求图片的资源,根据这个原理我们可以使用html5的data-xxx属性来存储图片的路径,在需要加载图片的时候,将data-xxx图片的路径赋值给src,这样就完成了图片的按需加载。
那么问题来了,什么时候加载后面的图片呢?
就是在我们滚动滚动轴的时候,当下一张图片的顶部马上要出现在视口的时候去加载下一张图片。
好啦,现在我们已经知道该什么去加载下一张图片了。那么接下来是第二个问题:怎么知道下一张图片马上要出现在视口上了呢?
就是当图片距离页面顶部的高度—即
:::tips
**offsetTop<= 视口高度clientHeight + 滚动条的长度scrollTop **
:::
这是重点!!!明白了这个后面就简单了
最后一个问题,如何获取offsetTop、scrollTop、clientHeight这三个数值呢?
- offsetTop:直接通过img.offsetTop就可以获取;
- scrollTop:通过document.documentElement.scrollTop获取;
- clientHeight:通过document.documentElement.clientHeight获取;
3.1.1 传统方式实现
function lazyload() {
const imgs = document.querySelectorAll("img[data-src][lazyload]");
let scrollTop = document.documentElement.scrollTop ;
let winHeight = document.documentElement.clientHeight;
imgs.forEach((item) => {
if (item.offsetTop < winHeight + scrollTop) {
item.src = item.getAttribute("data-src");
//移除属性,下次不再遍历
item.removeAttribute("data-src");
item.removeAttribute("lazyload");
}
});
}
lazyload();
window.addEventListener("scroll", lazyload);
上述方式存在严重的性能问题,比如说对于频繁触发的scroll事件会给浏览器造成很大的压力,以及频繁操作dom。我们可以使用节流函数做一个优化。
其他实现方案:
3.1.2 img标签loading=’lazy’属性
<img src="./example.jpg" loading="lazy" >
延迟加载图像,直到它和视口接近到一个计算得到的距离(由浏览器定义)。目的是在需要图像之前,避免加载图像所需要的网络和存储带宽。这通常会提高大多数典型用场景中内容的性能。
**缺点:**加载时机由浏览器自定义,加载顺序不定,存在未知性,且兼容性差。
3.1.3 IntersectionObserver
IntersectionObserver API(交叉观察者)会注册一个回调函数,每当被监视的元素进入或者退出另外一个元素时,或者两个元素的相交部分大小发生变化时,该回调方法会被触发执行。这样,我们网站的主线程不需要再为了监听元素相交而辛苦劳作,浏览器会自行优化元素相交管理。
// IntersectionObserver的属性
const options = {}
const intersectionObserver = new IntersectionObserver(function(entries, observer) {
entries.forEach(entry => {
// 触发的时间
entry.time
// 根元素的位置矩形,这种情况下为视窗位置
entry.rootBounds
// 被观察者的位置矩形
entry.boundingClientRect
// 是否重叠
entry.isIntersecting
// 重叠区域的位置矩形
entry.intersectionRect
// 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算)
entry.intersectionRatio
// 目标元素
entry.target
});
}, options);
// start observing
intersectionObserver.observe(document.querySelector('.element'));
// stop observing
intersectionObserver.unobserve(document.querySelector('.element'));
懒加载实现
function IOLazyload() {
const io = new IntersectionObserver((entries) => {
entries.forEach((item) => {
// isIntersecting是一个Boolean值,判断目标元素当前是否可见
if (item.isIntersecting) {
item.target.src = item.target.dataset.src; // 图片加载后即停止监听该元素
io.unobserve(item.target);
}
});
});
const imgs = document.querySelectorAll("img[data-src][lazyload]");
imgs.forEach((element, index) => {
console.log(element, index);
io.observe(element);
});
}
IOLazyload();
对浏览器兼容性要求不高的话,推荐使用IntersectionObserver方法,性能较好。
3.2 路由懒加载
官方解释:在SPA应用中,一个路由对应一个页面,如果我们不做任何处理,项目打包时,所有的页面都会打包成一个文件,JavaScript 包会变得非常大,影响页面加载。如果我们能把不同路由对应的组件分割成不同的代码块,然后当路由被访问的时候才加载对应组件,这样就会更加高效。
:::tips
懒加载或者按需加载,是一种很好的优化网页或应用的方式。这种方式实际上是先把你的代码在一些逻辑断点处分离开,然后在一些代码块中完成某些操作后,立即引用或即将引用另外一些新的代码块。这样加快了应用的初始加载速度,减轻了它的总体体积,因为某些代码块可能永远不会被加载。
:::
3.2.1 路由懒加载的实现
常规路由写法: 打包时会将所有的组件内容都打包进同一个js文件
import Login from '@/views/Login'
export default new Router(({
routes:[
{
path:'/login',
name:'login',
compontent: Login
}
]
})
方式一:ES6模块语法,ES6中有一个动态加载模块的方法:import(),Vue Router 支持开箱即用的动态导入,这意味着你可以用动态导入代替静态导入:
export default new Router(({
routes:[
{
path:'/login',
name:'login',
compontent: () => import('@/views/Login')
}
]
})
**component **配置可以接收一个返回 Promise 组件的函数。
方式二:commonJS模块语法。使用webpack特定的require.ensure,是webpack的遗留功能,已被 import() 取代。
export default new Router(({
routes:[
{
path:'/login',
name:'login',
compontent: resolve => require.ensure([], () => resolve(require('@/views/Login')), 'login')
}
]
})
方式三:使用AMD规范的require写法。
const router = new Router({
routes: [
{
path: '/list',
component: resolve => require(['@/views/Login'], resolve)
}
]
})
3.2.2 实现原理
懒加载的前提
:::tips
进行懒加载的子模块(子组件)需要是一个单独的文件
:::
因为懒加载是对子模块(子组件)进行延后加载。如果子模块(子组件)不单独打包,而是和别的模块合并在一起,那么当其他模块加载时就会将整个文件加载。那么如此就达不到懒加载的效果。
因此,第一步就是将懒加载的子模块(子组件)分离出来
懒加载前提的实现:ES6的动态地加载模块——import()。
:::tips
调用 import() 之处,被作为分离的模块起点,意思是,被请求的模块和它引用的所有子模块,会分离到一个单独的 chunk 中。 ——摘自《webpack——模块方法》的import()小节
:::
简单来讲就是,通过import()引用的子模块会被单独分离出来,打包成一个单独的文件(打包出来的文件被称为chunk )
独立打包——代码演示
有时候我们想把某个路由下的所有组件都打包在同个异步块 (chunk) 中。只需要使用命名 chunk,一个特殊的注释语法来提供 chunk name (需要 Webpack > 2.4):
// 例子2: 打包后将带有 webpackChunkName 配置的组件按照webpackChunkName分组打包在同一个js文件里
export default new Router(({
routes:[
{
path:'/test1',
name:'test1',
compontent: () => import(/* webpackChunkName: "group-foo" */ '@/views/Test1')
},
{
path:'/test2',
name:'test2',
compontent: () => import(/* webpackChunkName: "group-foo" */ '@/views/Test2')
}
]
})
webpack 会将任何一个异步模块与相同的块名称组合到相同的异步块中。
异步操作实现懒加载
基于上一步独立打包的基础上,只用实现异步操作——借助函数来实现延迟执行子模块的加载代码,即可真正实现懒加载。
// main.js
window.onload = () => {
const btn = document.querySelector('#lazyLoad');
btn.onclick = () => import(/* webpackChunkName: "current" */'./current.js')
}
同理,路由文件也是如此,把按需加载的组件进行单独打包,在路由页面跳转时加载相应chuck即可。
可以看出,路由懒加载的根本原理是基于代码拆分,去做按需加载。至于按模块、路由还是更细粒度去做拆分,取决于项目本身,如果系统本身加载速度ok,直接通过静态引入也是没有问题的。
4 总结
懒加载涉及到的场景很多,但其根本思想无非就是将各种资源去做一个延迟加载,实现的方法也有多种,比如定时器、事件监听、回调函数、代码拆分等等,没有哪种方法一定是最好的,只要适合项目本身就好。我们需要学习掌握的主要是用懒加载的思想,去做一些优化处理。
参考
juejin.cn/post/720057…
juejin.cn/post/708722…
www.jianshu.com/p/af09f431b…