惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

Attack and Defense Labs
Attack and Defense Labs
V
Visual Studio Blog
云风的 BLOG
云风的 BLOG
爱范儿
爱范儿
Hugging Face - Blog
Hugging Face - Blog
GbyAI
GbyAI
有赞技术团队
有赞技术团队
J
Java Code Geeks
小众软件
小众软件
WordPress大学
WordPress大学
T
The Blog of Author Tim Ferriss
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
酷 壳 – CoolShell
酷 壳 – CoolShell
B
Blog
U
Unit 42
大猫的无限游戏
大猫的无限游戏
G
Google Developers Blog
I
InfoQ
Engineering at Meta
Engineering at Meta
F
Fortinet All Blogs
IT之家
IT之家
博客园 - Franky
腾讯CDC
罗磊的独立博客
雷峰网
雷峰网
M
MIT News - Artificial intelligence
博客园 - 司徒正美
A
About on SuperTechFans
SecWiki News
SecWiki News
Project Zero
Project Zero
T
Tenable Blog
The Last Watchdog
The Last Watchdog
Security Latest
Security Latest
Google DeepMind News
Google DeepMind News
C
Cyber Attacks, Cyber Crime and Cyber Security
PCI Perspectives
PCI Perspectives
博客园 - 【当耐特】
C
Check Point Blog
F
Full Disclosure
MongoDB | Blog
MongoDB | Blog
H
Help Net Security
Google Online Security Blog
Google Online Security Blog
T
Tor Project blog
T
Threat Research - Cisco Blogs
AI
AI
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
Application and Cybersecurity Blog
Application and Cybersecurity Blog
美团技术团队
Spread Privacy
Spread Privacy

元视角

.NET 生态下的 Agent 框架选型:从 ReAct 到原生推理 - 元视角 从「能用」到「好用」:LLM 流式响应实现方式的探索之路 - 元视角 当我用 2000 条聊天记录,让 AI 为我画一幅自画像 - 元视角 基于 Supabase 的 AI 应用开发探索 - 元视角 微博 × MCP:社交媒体新玩法解锁 - 元视角 四点钟海棠花未眠 - 元视角 Semantic Kernel × MCP:智能体的上下文增强探索 - 元视角 基于 K-Means 聚类分析实现人脸照片的快速分类 - 元视角 容器技术驱动下的代码沙箱实践与思考 - 元视角 温故而知新:后端通用查询方案的再思考 - 元视角 浅议 CancellationToken 在前后端协同取消场景中的应用 - 元视角 Semantic Kernel 视角下的 Text2SQL 实践与思考 - 元视角 关于 ChatGPT 的流式传输,你需要知道的一切 - 元视角 RAG 的是与非、Rewrite 和 Rerank - 元视角 使用 EFCore 和 PostgreSQL 实现向量存储及检索 - 元视角 基于 LLaMA 和 LangChain 实践本地 AI 知识库 - 元视角 使用 llama.cpp 在本地部署 AI 大模型的一次尝试 - 元视角 如何为 Git 配置多个 SSH Key - 元视角 C# 使用 LibUsbDotNet 实现 USB 设备检测 - 元视角 基于 C# 实现样式与数据分离的打印方案 - 元视角 基于 SVG 的图形交互方案实践 - 元视角 前端视频播放技术概览 - 元视角 温故而知新,再话 Python 动态导入 - 元视角 后 GPT 时代,NLP 不存在了? - 元视角 视频是不能 P 的系列:使用 Milvus 实现海量人脸快速检索 - 元视角 GDI+下字体大小自适应方案初探 - 元视角 小爱音箱集成 ChatGPT 的不完全教程 - 元视角 程序员视角下的三体世界随想 - 元视角 关于 Docker 容器配置信息的渐进式思考 - 元视角 在 Docker 容器内集成 Crontab 定时任务 - 元视角 为你的服务器集成 LDAP 认证 - 元视角 似花还似非花 - 元视角 视频是不能 P 的系列:使用 Dlib 实现人脸识别 - 元视角 浅议分布式链路追踪与日志的整合 - 元视角 关于 Git 大文件上传这件小事 - 元视角 .NET 进程内队列 Channel 的入门与应用 - 元视角 使用 Fody 实现 .NET 的静态编织 - 元视角 .NET Core + ELK 搭建可视化日志分析平台(下) - 元视角 支持外部链接跳转的 Vue Router 扩展实现 - 元视角 视频是不能 P 的系列:OpenCV 和 Dlib 实现表情包 - 元视角 不得不说的 ASP.NET Core 集成测试 - 元视角 再议 DDD 视角下的 EFCore 与 领域事件 - 元视角 Vue.js 前端项目容器化部署实践极简教程 - 元视角 再见,人间四月天 - 元视角 Python 图像风格化迁移助力画家梦想 - 元视角 利用 ASP.NET Core 中的标头传播实现分布式链路追踪 - 元视角 利用 gRPC 实现文件的上传与下载 - 元视角 七种武器:延迟队列的原理和实现总结 - 元视角 gRPC 流式传输极简入门指南 - 元视角 Envoy 集成 Jaeger 实现分布式链路追踪 - 元视角 浅议非典型 Web 应用场景下的身份认证 - 元视角 gRPC 借助 Any 类型实现接口的泛化调用 - 元视角 分布式丛林探险系列之 Redis 集群模式 - 元视角 分布式丛林探险系列之 Redis 主从复制模式 - 元视角 通过 Python 预测 2021 年双十一交易额 - 元视角 gRPC 搭配 Swagger 实现微服务文档化 - 元视角 SSL/TLS 加密传输与数字证书的前世今生 - 元视角 使用 Python 自动识别防疫健康码 - 元视角 你不可不知的容器编排进阶技巧 - 元视角 ASP.NET Core 搭载 Envoy 实现 gRPC 服务代理 - 元视角 再话 AOP,从简化缓存操作说起 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务身份认证(JWT) - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的监控预警 - 元视角 ASP.NET Core 搭载 Envoy 实现微服务的反向代理 - 元视角 ASP.NET Core gRPC 打通前端世界的尝试 - 元视角 EFCore 实体命名约定库:EFCore.NamingConventions - 元视角 ASP.NET Core gRPC 集成 Polly 实现优雅重试 - 元视角 ASP.NET Core gRPC 健康检查的探索与实现 - 元视角 ASP.NET Core gRPC 拦截器的使用技巧分享 - 元视角 SnowNLP 使用自定义语料进行模型训练 - 元视角 使用 HttpMessageHandler 实现 HttpClient 请求管道自定义 - 元视角 ABP vNext 的实体与服务扩展技巧分享 - 元视角 ABP vNext 对接 Ant Design Vue 实现分页查询 - 元视角 源代码探案系列之 .NET Core 跨域中间件 CORS - 元视角 源代码探案系列之 .NET Core 限流中间件 AspNetCoreRateLimit - 元视角 源代码探案系列之 .NET Core 并发限制中间件 ConcurrencyLimiter - 元视角 通过 EmbededFileProvider 实现 Blazor 的静态文件访问 - 元视角 低代码,想说爱你不容易 - 元视角 记一次失败的 ThoughtWorks 面试经历 - 元视角 从 C# 1.0 到 C# 9.0,历代 C# 语言特性一览 - 元视角 通过 Python 分析 2020 年全年微博热搜数据 - 元视角 基于 Python 和 Selenium 实现 CSDN 一键三连自动化 - 元视角 使用多线程为你的 Python 爬虫提速的 N 种姿势,你会几种? - 元视角 实现网页长截图的常见思路总结 - 元视角 温故而知新,由 ADO.NET 与 Dapper 所联想到的 - 元视角 视频是不能 P 的系列:OpenCV 人脸检测 - 元视角 作为技术宅的我,是这样追鬼滅の刃的 - 元视角 使用 Python 抽取《半泽直树》原著小说人物关系 - 元视角 厉害了!打工人用 Python 分析西安市职位信息 - 元视角 使用 dotTrace 对 .NET 应用进行性能分析与优化 - 元视角 一道 HashSet 面试题引发的蝴蝶效应 - 元视角 基于选项模式实现.NET Core 的配置热更新 - 元视角 Dapper.Contrib 在 Oracle 环境下引发 ORA-00928 异常问题的解决 - 元视角 .NET Core 中对象池(Object Pool)的使用 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(下):EventBus 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(中):RabbitMQ 篇 - 元视角 利用 MySQL 的 Binlog 实现数据同步与订阅(上):基础篇 - 元视角 记一次从已损坏的 Git 仓库中找回代码的经历 - 元视角 .NET Core 原生 DI 扩展之属性注入实现 - 元视角 .NET Core 原生 DI 扩展之基于名称的注入实现 - 元视角
聊一聊前端图片懒加载背后的故事 - 元视角
飞鸿踏雪 · 2022-08-03 · via 元视角

相信大家已经注意到我博客有了一点变化,因为博主最近利用空闲时间对博客进行了优化。经过博主的不懈努力,首屏渲染时间从原来的 2.0 秒缩短到了 1.7 秒。虽然这个优化相当得感人,不过我还是在这个过程中有所收获。Stack 这个主题中大量使用了图片这种元素,特别是首页中那些作为文章封面而存在的图片。我原本是打算借鉴一下 Wincer 这位网友的博客样式,可是考虑到选择封面、图片尺寸…等等的因素,我最终还是决定写一个相对“平庸”的布局样式,即你现在看到的这个版本,本次优化的重点主要在于使用 CDN 加速、对图片进行压缩、编译期生成缩略图、使用懒加载这些常见的策略。在今天这篇博客中,我们来重点聊一聊前端图片的懒加载,希望能为大家带来一点新的启发或者思考。

什么是懒加载

懒加载,即:LazyLoad,其核心全在于“懒”这个字眼上。虽然,这个字在生活中更多的是表示一种贬义,可正如气体有活性和惰性的区别,这里我们将其理解为延迟加载,或许会更合适一点,因为生活早已告诉我们,只要你打算偷懒,就一定会造成拖延。因此,懒加载其实就是一种通过延迟加载对网页性能进行优化的方法。一个典型的例子是,当网页中有滚动条的时候。此时,网页的一部分区域对于浏览器视窗而言是不可见的。如果将一次性将其加载出来,这其实是一种资源的浪费,因为你不确定用户是否有耐心浏览完整个网页。在对网页的浏览量进行评估的时候,通常都会有一个跳出率的概念。可想而知,用户更容易被网页上的超链接吸引,在不同的网页间跳转。退一步讲,如果一个网页上有非常多的图片,等待这些图片全部加载完会浪费大量时间,进而影响到用户体验。博主原本就是为了减少首屏渲染时间,所以,不管从哪一个角度来看,懒加载或者说延迟加载,对于前端的性能优化都有着极其重要的意义,而这正是博主写作这篇文章的原始动机所在。

骨架屏利用懒加载来提升用户体验 骨架屏利用懒加载来提升用户体验

如何实现懒加载

我们知道,对于图片而言,我们只要设置了其 src 属性,它就可以自动载入图片。因此,图片的懒加载,其实就是让设置 src 属性这个行为延迟执行,譬如,当一张图片出现在用户的视野当中的时候,我们再去设置其 src 属性,这样就可以达到延迟加载的目的。显然,首次需要加载的图片数量越少,首屏渲染时间就会越短,这不正是我们想要达到的目的吗?基于这种朴实无华的思路,这里我们介绍三种实现延迟加载的方案,如果大家还有更好的方案,欢迎大家在评论区补充或者讨论。

监听滚动事件

首先,我们最容易想到的一种思路是,监听网页的滚动事件,因为我们更希望看到的结果是,当元素滚动到可视视口内的时候再去加载。此时,问题的关键是如何判断当前元素在可视视口内,在解决这个问题之前,我们先来看看下面这幅图片,它展示了网页中的 clientHeightscrollTop 以及 offsetTop 这三个数值间的关系:

clientHeight、scrollTop 以及 offsetTop clientHeight、scrollTop 以及 offsetTop

可以注意到,当 clientHeight(H) + scrollTop(S) > offsetTop 的时候,即表示当前元素位于可视视口内。基于这个思路,我们可以编写出下面的代码:

let lazyLoadByDefault = function(imgs) {
    var H = document.documentElement.clientHeight;
    var S = document.documentElement.scrollTop || document.body.scrollTop;
    for (var i = 0; i < imgs.length; i++) {
        if (H + S > getTop(imgs[i])) {
            if (imgs[i].getAttribute('data-loading') == 'lazy' && 
                imgs[i].getAttribute('data-src')) 
            {
                let src = decodeURI(imgs[i].getAttribute('data-src'))
                imgs[i].src = src
                imgs[i].removeAttribute("data-loading")
            }
        }
    }
}

window.onload = window.onscroll = function() {
    var imgs = document.querySelectorAll('img');
    lazyLoadByDefault(imgs)
}

其中,getTop() 方法用于计算 offsetTop,为什么不直接使用这个值呢,因为这个值是相对于父元素而言的,所以,考虑到元素嵌套的问题,我们必须要计算出每一个层级相对于父级的 offsetTop,然后再将它们累加起来。除此之外,我们给每个 Img 元素增加了一个自定义属性 data-src,它里面放置的就是真正的图片地址,我们只要在合适的时机将其赋值给 src 即可。当然,为了效果更好一点,你可以准备一张表示 loading 的图片放在 src 属性上:

let getTop = function(e) {
    var T = e.offsetTop;
    
    while(e = e.offsetParent) {
        console.log(e)
        T += e.offsetTop;
    }
    return T;
}

相应地,此时我们需要像下面这样来准备 HTML 结构:

<img src="./imgs/loading.gif" data-src="./imgs/1.jpg" data-loading="lazy" alt="1" />

考虑到滚动事件可以引起图片的重复加载,这里采用的方案是:增加一个一个自定义属性 data-loading,并在加载完后移除该属性。定义这样一个属性的原因,一定程度上是为了避免和 loading='lazy' 撞车,而关于这个特性,我们会在下面的内容中做进一步的讲解。在这里,如果你对于 clientHeightscrollTop 以及 offsetTop 这三个数值一脸懵逼的话,我们还有一种稍微简单一点的做法,即调用 getBoundingClientRect() 这个方法,它会返回当前元素相对于可视视口的位置信息。此时,只要满足 top < clientHieght 这个条件即可。因此,上面的代码可以进一步简化为:

let lazyLoadByDefault = function(imgs) {
    var H = document.documentElement.clientHeight;
    for (var i = 0; i < imgs.length; i++) {
        var bounding = imgs[i].getBoundingClientRect()
        if (bounding.top <= H) {
            if (imgs[i].getAttribute('data-loading') == 'lazy' && 
                imgs[i].getAttribute('data-src')) 
            {
                let src = decodeURI(imgs[i].getAttribute('data-src'))
                imgs[i].src = src
                imgs[i].removeAttribute("data-loading")
            }
        }
    }
}

下面是博主编写的一个简单示例,仅供大家参考:

See the Pen Marker-Clusterer by qinyuanpei (@qinyuanpei) on CodePen.

IntersectionObserver

除了使用上面的手工计算的方式,我们还可以使用 IntersectionObserver 这个 API。关于这个 API,官方是这样描述的: IntersectionObserver 接口提供了一种异步观察目标元素与其祖先元素或顶级文档视窗交叉状态的方法。祖先元素与视窗被称为根。我们尝试将其翻译成人话,大意就是说,这个 API 可以判断目标元素是否与顶级文档视窗交叉(重叠),两者重叠其实就是说目标元素在可视视口内。一旦理解了这一点,我们就可以轻而易举地写出下面的代码:

let lazyLoadByObserver = function(lazyImages) {
    let lazyImageObserver = new IntersectionObserver(function(entries, observer) {
        entries.forEach(function(entry) {
          if (entry.isIntersecting) {
            let lazyImage = entry.target;
            if (lazyImage.getAttribute('data-loading') == 'lazy' && 
                lazyImage.getAttribute('data-src')) 
            {
                let src = decodeURI(lazyImage.getAttribute('data-src'))
                lazyImage.src = src
                lazyImage.removeAttribute("data-loading")
                lazyImageObserver.unobserve(lazyImage);
            }
          }
        });
      });
  
      lazyImages.forEach(function(lazyImage) {
        lazyImageObserver.observe(lazyImage);
      });
}

var imgs = document.querySelectorAll('img');
lazyLoadByObserver(imgs);

可以注意到,我们会尝试去观察每一个 Img 元素,当它和可视视口交叉(重叠)时,表明它正位于可视视口内,此时,我们会从 data-src 属性上读取图片的地址,然后将其赋值给 src 属性。这样,我们就实现了图片的懒加载。我个人认为,这个 API 非常好用,考虑到第一种方案,你可能会因为搞不清楚那些数值而导致计算上的错误,这个方案就相对简单一点。当然,你真正应该考虑的是,它的兼容性如何:

IntersectionObserver 兼容性 IntersectionObserver 兼容性

从这张图中可以看出,除了寿终正寝的 IE 浏览器,一腔孤勇的 Safari 浏览器,这个 API 的兼容性还是挺不错的。如果你依然对这一点感到如履薄冰,可以使用下面的代码来进行兼容性判断:

var imgs = document.querySelectorAll('img');
if ("IntersectionObserver" in window) {
    lazyLoadByObserver(imgs)
} else {
    lazyLoadByDefault(imgs)
}

相信你已经猜到,博主的博客其实是混合着使用了上面两种方案,在使用懒加载以后,首页打开的时候将不会再加载所有封面图片,而是等到这些封面图片出现在可视视口内的时候再去加载,正是这一点点微不足道的工作,让博客的首屏渲染时间缩短了 0.3 秒,我想说,这实在有趣!

See the Pen Marker-Clusterer by qinyuanpei (@qinyuanpei) on CodePen.

浏览器原生方案

原生懒加载在不同浏览器中的支持情况 原生懒加载在不同浏览器中的支持情况

Chrome 77Firefox 75 及其以上版本开始,浏览器开始支持图片和框架的原生懒加载特性。这意味着,从这一刻开始,我们有了浏览器级别的原生懒加载方案,即:在 <img> 或者 <iframe> 标签上添加 loading='lazy' 这组属性即可,下面是一个非常朴素的示例:

<img src="./example.jpg" loading="lazy" alt="this is a example for image lazy loading.">

由于是浏览器级别的懒加载方案,所以,它不需要我们再像上面两种方案一样,编写额外的 JavaScript 代码。事实上,loading 除了 lazy 这个取值以外,它还有下面两个取值:

  • lazy:图片或者框架使用懒加载,即元素即将可见的时候加载,且浏览器内部会定义一个元素和可视视口的距离的阈值,越接近该值表明元素越可见。
  • eager:立即加载图像或者框架,无论元素是否在可视视口内可见。
  • auto: 默认值,当元素没有显式地设置 loading 属性或者 loading 属性不合法的时候采用,此时图片或者框架会按照浏览器自己的策略来加载,需要注意的是,目前该值已被废弃。

这个方案应该是三种方案中最简单的,可实际中还是多多少少有一些问题,譬如没有办法给定一个占位图片、每次加载图片的数量与屏幕高度、网速、窗口尺寸等因素有关,甚至连加载图片的顺序都是不确定的,这就意味着这中间有很多难以控制的因素,如果考虑到兼容性或者 Polyfill 的问题,这个方案就显得没那么有吸引力了,我在测试的过程中发现,有时候它是一次性就把多张图片加载出来了。所以,我个人的观点是,想偷懒的话可以直接用这个特性,可是如果要控制懒加载过程中的细节,这个特性就显得非常鸡肋了。

See the Pen Marker-Clusterer by andypotts (@andypotts) on CodePen.

如图所示,随着你滑动鼠标滚轮,你会注意到它在加载指定的图片,这就是原生懒加载的基本用法啦!

本文小结

本文主要分享了前端图片懒加载的三种实现思路,即监听滚动事件、IntersectionObserver 以及浏览器原生支持的 loading='lazy'。懒加载的基本思路是延迟加载,对图片而言,我们更希望它可以在即将出现在用户视野中的时候去加载,因为这样能减少不必要的资源请求,同时可以缩短首屏渲染时间。因此,图片的懒加载是前端性能优化过程中不可或缺的一种优化策略。判断一个图片是否位于可视视口内,可以采用手工计算的方式,当然这里更推荐使用 IntersectionObserver 这个 API。 loading='lazy' 是一种浏览器级别的懒加载的特性,虽然它的用法非常简单,可是考虑到整个懒加载的过程,对用户而言完全就是个黑箱,因此,如果你想更精确地控制懒加载的细节,譬如给定一个占位图片,这种情况下该方案就显得非常鸡肋啦。更不必说,它里面有很多不确定的或者难以控制的因素,我个人觉得前两种方案结合起来会更好一点。好了,以上就是这篇博客的全部内容啦,谢谢大家!