使用过 Medium 的同学都或许会惊叹于 Medium 流畅的图片懒加载体验。于是我就想,能否在 Hexo 博客中也达到这样的一个 lazyload 效果呢?在参考了不少资料(特别是前辈产出的博文)后,得到一个还令我满意的 Hexo 图片懒加载尝试。
时隔多日的更新:此时距离我重新部署 lazyload 已经过去了有一阵子了,从谷歌统计数据看来,网站 DOMContentLoaded 提升近 1s 。懒加载带来如此明显的性能提升还是有点超出我的预料,前端性能方面也确实值得我们重视。
如果在搜索引擎中输入「hexo + lazyload」,出来的多半是让你下载形形色色的插件、甚至叫你引入一些奇奇怪怪的第三方库之类的教程。
这时许多教程都指向一个插件——Hexo lazyload image,当然还是要承认这是一个非常优秀的插件。但是仔细翻阅源代码后发现,Hexo lazyload image 使用的是性能耗费巨大的 scroll 而非我认为比较合适的 IntersectionObserver 实现。
是否真的值得为了那不到 7% 的 IE 用户做出如此大的牺牲呢?更何况这种实现方案在 RSS 等爬取工具中就只能得到一个 lazyload.png ,还是十分影响体验的。
不过好在也有人分享了一种更加现代的实现方案,不仅能充分发挥现代浏览器的优势,还能在老旧设备和 RSS 等工具中不进行 lazyload 、直接加载原图。
这里就用到了 srcset 这个关键字,srcset 和 src 属性一样,可是优先级更高。
<img src="origin.png"
srcset="thumbnail.png"
data-srcset="origin.png">
这样,即便我们不对 src 动刀,现代浏览器还是会先加载放在 srcset 中的缩略图 thumbnail.png,比如我就放了一个 1x1 的 base64 小图。而利用 lazyload JS 监控元素可视情况,当图片出现在可视范围内时候就将 data-srcset 赋给 srcset 显示原图 origin.png 。而在不兼容 srcset 的历史浏览器中和 RSS 等工具中,直接加载 src 属性显示原图,从而避免放着张 lazyload.png 在 RSS 阅读器里的尴尬。
你可以从https://caniuse.com/#feat=srcset这里,查看浏览器关于 srcset 的兼容情况。
简单粗暴的塞入图片往往导致突兀的体验,这其中还有些许细节值得留意。
Medium 加载原图的时候还有一个细节,那就是让图片有一个模糊变清晰的展现效果。个人感觉这样比把 lazyload.png 突兀地换成原图要自然一点。
实现也很简单,只需要添加几个 CSS:
img.lazyload {
transition: filter 0.375s ease 0s;
}
img.lazyload:not(.loaded) {
-webkit-filter: blur(8px);
filter: blur(8px);
}
我在懒加载的图片中添加了 class="lazyload" 然后在原图加载完毕时追加成 class="lazyload loaded" ,代码实现会在下一节放上参考。
图片 lazyload 还带来的一个问题就是布局抖动。由于一开始浏览器根本没有加载原图,也不会给图片自动留好一个位置。所以在加载原图的时候势必会导致窗口布局的抖动。
如果网站的图片并非同一尺寸的,加载大小不一的占位图势必会影响加载速度。我们可以调用 Aspect Ratio Boxes 响应式容器来分配占位空间。
但是我的图片和博客是分开存放的,所以也没法在部署时单独计算占位空间。
不过有一点我还是可以优化,那就是每篇文章的缩略图,我制作的缩略图都是 1280x512 尺寸的。针对这个制作占位空间,至少首页和文章头部就不会再抖动了。
img.lazyload:not(.loaded) { padding-bottom: 40.000% }
里面的数值根据图片宽高比例得出。
2022 年的更新:
即便是远程图片,其实也同样是可以做到计算长宽比的。例如 probe-image-size 等项目都很好的实现在不下载整张图片的前提下获取图片尺寸数据。
但由于 Hexo 进行过滤的 Script 是同步执行的,而 probe-image-size 推荐异步执行。一步异步必将导致步步异步。本人又没有深耕 Hexo 项目,这次就偷个懒采用同步执行,也就放弃了 probe-image-size 无需下载整张图的特性。
const probe = require('probe-image-size');
const request = require('sync-request');
let imageSize = probe.sync(request('GET', p2).getBody());
然后用 Element 的 Style 参数赋予对应尺寸参数,这里就用到了 aspect ratio。
<img
style="
width: ${imageSize.width + imageSize.wUnit},
aspect-ratio: ${imageSize.width + ' / ' + imageSize.height}
"
src=/* ... */
/* others... */
/>
这里只定义宽度不定义高度,是由于图片在页面中往往是宽度受限、高度可以拓展(纵向可以划动)。如果定死宽高,即便浏览器会自动计算 aspect-ratio,最终显示依然是高度遵循 height 而宽度被压缩,从而长宽比被破坏。如果定义宽度和长宽比,即便宽度被限制,也依然是长宽比不变的按比例压缩,从而最终解决抖动问题。
然而——由于每次生成都要扫描所有图片,此项操作会严重拖慢生成速度。且每次生成都会重新请求所有图片的尺寸数据,重复无用功占比极大,还有待优化。
首先要给每个 <img> 配置 srcset 和 data-srcset 属性。给每个图片手动转换效率过于低下,可以考虑在 scripts/ 下放一个用于转换的脚本,每次 Generate 的时候就会自动转换。
// scripts/lazyload/index.js
'use strict'
if (!hexo.config.lazyload || !hexo.config.lazyload.enable) {
return;
}
if (hexo.config.lazyload.onlypost) {
hexo.extend.filter.register('after_post_render', require('./lib/process').processPost);
} else {
hexo.extend.filter.register('after_render:html', require('./lib/process').processSite);
}
// theme/scripts/lazyload/lib/process.js
'use strict';
function lazyProcess(htmlContent) {
let loadingImage = this.config.lazyload.loadingImage || 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAABlBMVEXMzMyWlpYU2uzLAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAACklEQVQImWNgAAAAAgAB9HFkpgAAAABJRU5ErkJggg==';
return htmlContent.replace(/<img(.*?)src="(.*?)"(.*?)>/gi, function (str, p1, p2) {
if(/data-srcset/gi.test(str)){
return str;
}
if(/src="data:image(.*?)/gi.test(str)) {
return str;
}
if(/no-lazy/gi.test(str)) {
return str;
}
let imageSize = probe.sync(request('GET', p2).getBody());
return str.replace(`src="${p2}"`, `src="${p2}" class="lazy" data-srcset="${p2}" srcset="${loadingImage}" style="width: ${imageSize.width + imageSize.wUnits}; aspect-ratio: ${imageSize.width + ' / ' + imageSize.height}"`);
});
});
}
module.exports.processPost = function(data) {
data.content = lazyProcess.call(this, data.content);
return data;
};
module.exports.processSite = function (htmlContent) {
return lazyProcess.call(this, htmlContent);
};
然后在每个需要用到图片的页面插入以下代码:
'use strict'
function query(selector) {
return Array.from(document.querySelectorAll(selector));
}
var io = new IntersectionObserver(function (entries) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
var img = entry.target;
img.srcset = img.getAttribute('data-srcset');
img.className += ' loaded'
io.unobserve(img);
}
});
});
query('img.lazyload').forEach(function (item) {
io.observe(item);
});
用于在每个页面添加将原图赋给 srcset 的 lazyload JS 。引入 Vanilla Lazyload 等库效果类似,唯独需要留心在加载完成后对应元素的属性变化,灵活操作例子中的 loaded 即可。
前端性能优化中的 lazyload 实现并不困难,但是还是有各种细节需要我们注意。
一般而言,图片可以更好地辅佐博主表达内容,但是多图流带来的加载噩梦,特别是在带宽远不及桌面端的移动端上,花费一点时间实现更优的图片懒加载,还是很有必要的。