author: 专注前端开发,分享JavaScript干货
title: JavaScript实战③|图片懒加载与无限滚动,性能优化技巧
update: 2026-04-28
tags: JavaScript,实战项目,懒加载,无限滚动,性能优化,IntersectionObserver
作者:专注前端开发,分享JavaScript干货
更新时间:2026年4月
适合人群:掌握JS基础,想学习性能优化技巧的开发者
前言:为什么需要懒加载?
网页加载大量图片时:
- 首屏加载慢
- 浪费用户流量
- 页面卡顿
懒加载(Lazy Loading)让图片进入视口才加载,大幅提升性能。
一、图片懒加载
1.1 传统方式(scroll 事件)
// 获取所有需要懒加载的图片constlazyImages=document.querySelectorAll('img[data-src]');functionlazyLoad(){lazyImages.forEach(img=>{// 判断图片是否进入视口constrect=img.getBoundingClientRect();constisInViewport=rect.top<window.innerHeight&&rect.bottom>0;if(isInViewport&&img.dataset.src){img.src=img.dataset.src;img.removeAttribute('data-src');}});}// 监听滚动事件(需要节流)window.addEventListener('scroll',throttle(lazyLoad,200));window.addEventListener('resize',throttle(lazyLoad,200));// 页面加载时执行一次lazyLoad();// 节流函数functionthrottle(fn,delay){letlastTime=0;returnfunction(...args){constnow=Date.now();if(now-lastTime>=delay){fn.apply(this,args);lastTime=now;}};}1.2 现代方式(IntersectionObserver)
// 使用 IntersectionObserver API(更现代、更高效)constlazyImages=document.querySelectorAll('img[data-src]');constimageObserver=newIntersectionObserver((entries,observer)=>{entries.forEach(entry=>{if(entry.isIntersecting){constimg=entry.target;img.src=img.dataset.src;// 加载完成后移除>.onload=()=>{img.removeAttribute('data-src');};// 停止观察这张图片observer.unobserve(img);}});},{rootMargin:'50px 0px',// 提前 50px 开始加载threshold:0.01});lazyImages.forEach(img=>imageObserver.observe(img));1.3 HTML 结构
<!-- 懒加载图片 --><imgdata-src="large-image.jpg"src="placeholder.jpg"alt="描述"class="lazy-image"><style>.lazy-image{opacity:0;transition:opacity 0.3s;}.lazy-image[src]:not([data-src]){opacity:1;}</style>二、无限滚动(Infinite Scroll)
2.1 实现原理
classInfiniteScroll{constructor(options){this.container=document.querySelector(options.container);this.loadMore=options.loadMore;this.loading=false;this.hasMore=true;this.page=1;this.init();}init(){// 使用 IntersectionObserver 监听底部元素constsentinel=document.createElement('div');sentinel.className='scroll-sentinel';sentinel.style.height='10px';this.container.appendChild(sentinel);this.observer=newIntersectionObserver((entries)=>{entries.forEach(entry=>{if(entry.isIntersecting&&!this.loading&&this.hasMore){this.loadNextPage();}});},{rootMargin:'100px 0px'});this.observer.observe(sentinel);// 初始加载this.loadNextPage();}asyncloadNextPage(){if(this.loading||!this.hasMore)return;this.loading=true;this.showLoading();try{constresult=awaitthis.loadMore(this.page);if(result.data.length===0){this.hasMore=false;this.showNoMore();}else{this.renderItems(result.data);this.page++;}}catch(error){console.error('加载失败:',error);this.showError();}finally{this.loading=false;this.hideLoading();}}showLoading(){constloader=document.createElement('div');loader.className='loading-indicator';loader.innerHTML='加载中...';this.container.appendChild(loader);}hideLoading(){constloader=this.container.querySelector('.loading-indicator');if(loader)loader.remove();}showNoMore(){constnoMore=document.createElement('div');noMore.className='no-more';noMore.innerHTML='没有更多内容了';noMore.style.cssText='text-align: center; padding: 20px; color: #999;';this.container.appendChild(noMore);}showError(){consterror=document.createElement('div');error.className='error-message';error.innerHTML='加载失败,<a href="#" onclick="location.reload()">点击重试</a>';this.container.appendChild(error);}renderItems(items){// 子类实现具体的渲染逻辑}}2.2 使用示例
// 图片列表无限滚动classImageGalleryextendsInfiniteScroll{constructor(){super({container:'.gallery',loadMore:async(page)=>{// 模拟 API 请求constresponse=awaitfetch(`/api/images?page=${page}`);returnresponse.json();}});}renderItems(images){consthtml=images.map(img=>`<div class="gallery-item"> <img>${img.url}" src="placeholder.jpg" alt="${img.title}"> <p>${img.title}</p> </div>`).join('');// 插入到 sentinel 之前constsentinel=this.container.querySelector('.scroll-sentinel');sentinel.insertAdjacentHTML('beforebegin',html);// 对新添加的图片启用懒加载constnewImages=this.container.querySelectorAll('img[data-src]');newImages.forEach(img=>imageObserver.observe(img));}}// 初始化constgallery=newImageGallery();三、完整示例:图片画廊
<!DOCTYPEhtml><htmllang="zh-CN"><head><metacharset="UTF-8"><title>图片懒加载 + 无限滚动</title><style>*{margin:0;padding:0;box-sizing:border-box;}body{font-family:sans-serif;background:#f5f5f5;}.gallery{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:20px;padding:20px;max-width:1200px;margin:0 auto;}.gallery-item{background:white;border-radius:8px;overflow:hidden;box-shadow:0 2px 8pxrgba(0,0,0,0.1);}.gallery-item img{width:100%;height:200px;object-fit:cover;opacity:0;transition:opacity 0.3s;background:#e0e0e0;}.gallery-item img.loaded{opacity:1;}.gallery-item p{padding:15px;color:#333;}.loading-indicator{grid-column:1 / -1;text-align:center;padding:40px;color:#666;}</style></head><body><divclass="gallery"id="gallery"></div><script>// 模拟图片数据constmockImages=[{url:'https://picsum.photos/400/300?random=1',title:'图片 1'},{url:'https://picsum.photos/400/300?random=2',title:'图片 2'},{url:'https://picsum.photos/400/300?random=3',title:'图片 3'},// ... 更多图片];// 懒加载 ObserverconstimageObserver=newIntersectionObserver((entries)=>{entries.forEach(entry=>{if(entry.isIntersecting){constimg=entry.target;img.src=img.dataset.src;img.onload=()=>{img.classList.add('loaded');img.removeAttribute('data-src');};imageObserver.unobserve(img);}});},{rootMargin:'50px'});// 无限滚动letpage=1;letloading=false;asyncfunctionloadMoreImages(){if(loading)return;loading=true;// 模拟 API 延迟awaitnewPromise(resolve=>setTimeout(resolve,1000));constgallery=document.getElementById('gallery');// 生成 10 张图片for(leti=0;i<10;i++){constrandomId=page*10+i;constitem=document.createElement('div');item.className='gallery-item';item.innerHTML=`<img>${randomId}" alt="图片${randomId}"> <p>图片${randomId}</p>`;gallery.appendChild(item);// 观察新图片imageObserver.observe(item.querySelector('img'));}page++;loading=false;}// 监听滚动constscrollObserver=newIntersectionObserver((entries)=>{entries.forEach(entry=>{if(entry.isIntersecting){loadMoreImages();}});},{rootMargin:'200px'});// 创建底部触发元素consttrigger=document.createElement('div');trigger.style.height='10px';document.body.appendChild(trigger);scrollObserver.observe(trigger);// 初始加载loadMoreImages();</script></body></html>四、知识点回顾
| 知识点 | 应用 |
|---|---|
IntersectionObserver | 监听元素是否进入视口 |
data-src | 存储真实图片地址 |
rootMargin | 提前触发加载的距离 |
throttle | 节流,优化滚动性能 |
async/await | 异步加载数据 |
五、课后作业
- 实现一个带骨架屏的懒加载图片组件
- 给无限滚动添加"加载更多"按钮作为降级方案
- 实现图片加载失败时的占位图显示
有问题欢迎评论区留言,大家一起讨论!
标签:JavaScript | 实战项目 | 懒加载 | 无限滚动 | 性能优化 | IntersectionObserver