Web 图片体积优化与懒加载
我最近为某产品介绍网页图片添加了 avif 和 webp 格式支持,相较于原有 png 图片在保持清晰度基本一致的情况下,图片文件大小有了显著的减小,加载速度也因此变得更快,其中 avif 表现更好。
这中间我转换图片格式使用到的工具是由 google 开发的 squoosh,他有网页版和命令行版可供使用,网页版的地址是 https://squoosh.app,命令行版已经不再继续维护,因此需要切换低版本 node.js 才能正常使用,node.js 14.15.0 经过测试是可用的,执行 npm i -g @squoosh/cli
安装,接着就可用以下命令转换图片格式:
squoosh-cli --oxipng '{"quality": 75}' ".\assets\img\home-page\life.png"
squoosh-cli --webp '{"quality": 100}' ".\assets\img\home-page\life.png"
squoosh-cli --avif '{"quality": 100}' ".\assets\img\home-page\life.png"
使用 <picture>
标签指定图片格式优先级,保证一定的兼容性。
<picture>
<source srcset="./assets/img/home-page/life.avif" type="image/avif" />
<source srcset="./assets/img/home-page/life.webp" type="image/webp" />
<img src="./assets/img/home-page/life.png" />
</picture>
现代浏览器中,在 <img>
标签中加上 loading="lazy"
属性即可实现图片的懒加载。
<picture>
<source srcset="./assets/img/home-page/life.avif" type="image/avif" />
<source srcset="./assets/img/home-page/life.webp" type="image/webp" />
<img src="./assets/img/home-page/life.png" loading="lazy" />
</picture>
这种实现懒加载的方法从实现难度上来讲是很容易的,可以满足一般的需求,但是对于旧版浏览器来讲兼容性有限,对于何时加载图片这个进入视口的时机也不能完全掌控,大概是距进入视口 1000px 左右就开始加载了,不同浏览器实现不是很一致,对于背景图片 background-image 的懒加载也是完全不支持的。
以下是在 Vue 中利用自定义指令实现的图片懒加载,也就是 JS 实现懒加载的方法:
const inBrowser = typeof window !== 'undefined' && window !== null
const hasIntersectionObserver = checkIntersectionObserver()
function checkIntersectionObserver() {
if (
inBrowser &&
'IntersectionObserver' in window &&
'IntersectionObserverEntry' in window &&
'intersectionRatio' in window.IntersectionObserverEntry.prototype
) {
// Minimal polyfill for Edge 15's lack of `isIntersecting`
// See: https://github.com/w3c/IntersectionObserver/issues/211
if (!('isIntersecting' in window.IntersectionObserverEntry.prototype)) {
Object.defineProperty(window.IntersectionObserverEntry.prototype, 'isIntersecting', {
get() {
return this.intersectionRatio > 0
}
})
}
return true
}
return false
}
function resolveAssetPath(path) {
if (path.startsWith('@/')) {
return new URL(path.replace('@/', '/src/'), import.meta.url).href
}
return path
}
function getResponsiveImg(path) {
const html = document.documentElement
if (html.classList.contains('avif')) {
return resolveAssetPath(`${path}.avif`)
}
if (html.classList.contains('webp')) {
return resolveAssetPath(`${path}.webp`)
}
return resolveAssetPath(`${path}.png`)
}
export default {
mounted(el, binding) {
const opts = (binding.value && binding.value.options) || {}
const rootMargin = opts.rootMargin ?? '200px'
const threshold = opts.threshold ?? 0
const defaultLoadingImg = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=='
if (el.tagName.toLowerCase() === 'img') {
el.setAttribute('src', defaultLoadingImg)
} else {
const img = el.querySelector('img')
if (img) {
el.style.display = 'inline-block'
img.setAttribute('src', defaultLoadingImg)
} else {
if (binding.arg === 'background') {
el.style.backgroundImage = `url(${defaultLoadingImg})`
}
}
}
const load = () => {
if (el.tagName.toLowerCase() === 'img') {
const src = el.dataset.src
if (src) {
el.setAttribute('src', resolveAssetPath(src))
}
} else {
const sources = el.querySelectorAll('source')
sources.forEach(source => {
const srcset = source.dataset.srcset
if (srcset) {
source.setAttribute('srcset', resolveAssetPath(srcset))
}
})
const img = el.querySelector('img')
if (img) {
const src = img.dataset.src
if (src) {
img.setAttribute('src', resolveAssetPath(src))
}
} else {
if (binding.arg === 'background') {
const bg = el.dataset.src
if (bg) el.style.backgroundImage = `url(${resolveAssetPath(bg)})`
}
}
}
}
if (!hasIntersectionObserver) {
load()
return
}
const observer = new IntersectionObserver(
(entries, obs) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
load()
obs.unobserve(entry.target)
}
})
},
{ rootMargin, threshold }
)
observer.observe(el)
el._lazyObserver = observer
},
unmounted(el) {
if (el._lazyObserver) {
el._lazyObserver.disconnect()
delete el._lazyObserver
}
}
}
下面是使用方法:
<!-- 第一种 -->
<picture v-lazy>
<source data-srcset="@/assets/img/home-page/life.avif" type="image/avif" />
<source data-srcset="@/assets/img/home-page/life.webp" type="image/webp" />
<img class="filter-animate" data-src="@/assets/img/home-page/life.png" />
</picture>
<!-- 第二种 -->
<div class="bg-head" v-lazy:background :data-src="getResponsiveImg('@/assets/img/home-page/life')"></div>
<!-- 第三种 -->
<img v-lazy :data-src="item.img" alt="" />
用 JS 实现需要更多代码支持,但也解决了 loading="lazy"
无法直接解决的问题,在实际应用中可依情况而定懒加载方案。