圖片性能優化
圖片懶加載
- 如何判斷圖片出現在了當前視口 (即如何判斷我們能夠看到圖片)
- 如何控制圖片的加載
原生實現
<img src="shanyue.jpg" loading="lazy" />
loading="lazy"
延遲加載圖像,直到它和視口接近到一個計算得到的距離(由瀏覽器定義)。目的是在需要圖像之前,避免加載圖像所需要的網絡和存儲帶寬。這通常會提高大多數典型用場景中內容的性能。
- lazy:對資源進行延遲加載。
- eager:立即加載資源。
- auto:瀏覽器自行判斷決定是否延遲加載資源。
通過相對計算獲取元素位置
圖片頂部到文檔頂部的距離 > 瀏覽器可視窗口高度 + 滾動條滾過的高度,此時的圖片就是不可見的,如果圖片頂部到文檔頂部的距離 < 瀏覽器可視窗口高度 + 滾動條滾過的高度那么該圖片就應該出現在可視區域內了。
但你還記得我們前面提到的注意事項嗎?如果用戶直接滑到頁面底部,那么這個判斷條件對所有的圖片都為真,還是會造成性能問題。所以我們要再加上一條判斷條件 圖片的高度 + 圖片頂部到文檔頂部的距離 > 滾動條滾過的高度,以確保圖片確實在可視區域內,而不只是被滑過。
- 待加載圖片的高度:
img.clientHeight
- 圖片頂部到文檔頂部的距離:
img.offsetTop
- 瀏覽器窗口滾動過的距離:
document.documentElement.scrollTop
或document.body.scrollTop
- 瀏覽器可視窗口高度:
document.documentElement.clientHeight
或window.innerHeight
const imgs = document.querySelectorAll('img')
function lazyLoad(imgs) {console.log('lazyLoad')// 瀏覽器可視窗口的高度const windowHeight = window.innerHeight// 可視窗口滾動過的距離const scrollHeight = document.documentElement.scrollTopfor (let i = 0; i < imgs.length; i++) {if (windowHeight + scrollHeight > imgs[i].offsetTop && imgs[i].clientHeight + imgs[i].offsetTop > document.documentElement.scrollTop && !imgs[i].src) {imgs[i].src = imgs[i].dataset.src}}
}
// 進入頁面時執行一次加載
lazyLoad(imgs)
// 監聽滾動事件,當滾動到可視區域時加載圖片
// 此處可以添加防抖/節流優化 window.onscroll = throttle(lazyLoad, 500)
window.onscroll = function () {lazyLoad(imgs)
}
Element.getBoundingClientRect()
getBoundingClientRect
返回值是一個 DOMRect 對象,這個對象是由該元素的 getClientRects()
方法返回的一組矩形的集合, 即:是與該元素相關的 CSS 邊框集合 。DOMRect 對象包含了一組用于描述邊框的只讀屬性——left、top、right 和 bottom,單位為像素。除了 width 和 height 外的屬性都是相對于視口的左上角位置而言的。
有了這個 API 后我們很同意獲取圖片的 top 值,當 top 值小于可視區的高度的時候就可以任何圖片進入了可視區,直接加載圖片即可。
document.addEventListener('DOMContentLoaded', () => {const lazyImages = document.querySelectorAll('img.lazyload')const lazyLoad = () => {lazyImages.forEach((img) => {if (img.getBoundingClientRect().top <= window.innerHeight && img.getBoundingClientRect().bottom >= 0 && getComputedStyle(img).display !== 'none') {img.src = img.dataset.srcimg.classList.remove('lazyload')}})if (lazyImages.length === 0) {document.removeEventListener('scroll', lazyLoad)window.removeEventListener('resize', lazyLoad)window.removeEventListener('orientationchange', lazyLoad)}}document.addEventListener('scroll', lazyLoad)window.addEventListener('resize', lazyLoad)window.addEventListener('orientationchange', lazyLoad)
})
使用 IntersectionObserver
<img data-src="xxx.jpg" class="lazyload" /><script>document.addEventListener('DOMContentLoaded', () => {const lazyImages = document.querySelectorAll('img.lazyload')if ('IntersectionObserver' in window) {const observer = new IntersectionObserver((entries, observer) => {entries.forEach((entry) => {if (entry.isIntersecting) {const image = entry.targetimage.src = image.dataset.srcimg.classList.remove('lazyload')observer.unobserve(image)}})})lazyImages.forEach((img) => {observer.observe(img)})} else {lazyImages.forEach((img) => {img.src = img.dataset.src})}})
</script>
監聽元素的重疊度 IntersectionObserver
var observer = new IntersectionObserver(callback[, options]);
IntersectionObserver
的disconnect()
方法終止對所有目標元素可見性變化的觀察。
IntersectionObserver
的observe()
方法向IntersectionObserver
對象觀察的目標集合添加一個元素。一個觀察者有一組閾值和一個根(root),但是可以監視多個目標元素的可見性變化(遵循閾值和根的設置)。
IntersectionObserver
的takeRecords()
方法返回一個IntersectionObserverEntry
對象數組,每個對象包含目標元素自上次相交檢查以來所經歷的相交狀態變化——可以顯式地通過調用此方法或隱式地通過觀察器的回調獲得。
IntersectionObserver
的unobserve()
方法命令IntersectionObserver
停止對一個元素的觀察。const ob = new IntersectionObserver( (entries) => {const entry = entries[0]if (entry.isIntersecting) {console.log('加載更多')} }, {// root 監聽元素的祖先元素Element對象,其邊界盒將被視作視口。目標在根的可見區域的任何不可見部分都會被視為不可見。root: null,// rootMargin 一個在計算交叉值時添加至根的邊界盒 (bounding_box) 中的一組偏移量,類型為字符串 (string) ,可以有效的縮小或擴大根的判定范圍從而滿足計算需要。語法大致和 CSS 中的margin 屬性等同; 可以參考 intersection root 和 root margin 來深入了解 margin 的工作原理及其語法。默認值是"0px 0px 0px 0px"。// threshold 規定了一個監聽目標與邊界盒交叉區域的比例值,可以是一個具體的數值或是一組 0.0 到 1.0 之間的數組。若指定值為 0.0,則意味著監聽元素即使與根有 1 像素交叉,此元素也會被視為可見。若指定值為 1.0,則意味著整個元素都在可見范圍內時才算可見。threshold: 0 })const dom = document.querySelector('.loading') ob.observe(dom)
使用庫
lazysizes、lazyload
圖片預加載
const images = ['https://picsum.photos/id/237/400/400.jpg?grayscale&blur=2','https://picsum.photos/id/238/400/400.jpg?grayscale&blur=2'
]function preloadImages(max = 3) {const _images = [...images]function loadImage() {const src = _images.shift()return new Promise((resolve, reject) => {const link = document.createElement('link')link.rel = 'preload'link.as = 'image'link.href = srcdocument.head.appendChild(link)link.onload = resolvelink.onerror = rejectsetTimeout(reject, 10000)})}function _loadImage() {loadImage().finally(() => {if (_images.length) {loadImage()}})}for (let i = 0; i < max; i++) {_loadImage()}
}