百萬PV商城實踐系列 - 前端圖片資源優化實戰

⚠️ 本文爲掘金社區首發簽約文章,未獲受權禁止轉載css

前言

百萬PV商城系列主要收錄我在商城項目中的實踐心得,我會從視覺體驗性能優化架構設計等三個維度出發,一步一步爲你們講解商城項目中前端出現的問題問題解決的思路與方案,最後再進行代碼實踐。讓你在工做中碰到相似問題時,可以更加駕輕就熟。前端

本篇是商城實踐系列的第一篇文章,主要內容是對商城項目中常見場景的圖片資源加載進行優化,提高視覺體驗與頁面性能。node

背景

在平常生活中,Yoyo們確定都用過一些電商公司的AppWeb小程序等應用。在瀏覽的過程當中,會有很是多琳琅滿目的圖片,好比常見的商品卡、商品詳情、輪播圖、 廣告圖等組件都須要使用一些後臺管理配置上傳的圖片資源。react

除此以外,這些組件所在的頁面也大都是流量承擔很是大的頁面,若是用戶的體驗感官比較差,必然會影響用戶的留存轉化。那麼,本篇文章咱們就來學習一下圖片資源優化的一些方案技巧,我會分別從普通圖片、高保真圖片、長屏渲染圖這三個場景,依次給你們講講工做中的實戰操做。若是碰到相應的優化場景,你就可以從容以對了。git

image.png

普通圖片優化

對於普通圖片優化,我以懶加載(lazyLoad)做爲主要的實施手段。懶加載說白了就是優先加載可視窗口內的圖片資源,而可視窗口外的內容只有滾動後進入可視窗口內纔會進行加載。shell

那麼,我爲何會用懶加載,以及懶加載的實現方案究竟是什麼呢?下面,我經過一個簡單的React的圖片懶加載方案的實現,來一步一步說明。小程序

爲何使用懶加載?

通常來講,剛進入網頁頁面就會有大批量的圖片資源加載,這會間接影響頁面的加載,增長白屏加載時間,影響用戶體驗。所以,咱們的訴求就是不在可視化窗口內的圖片不盡興加載,儘量減小本地帶寬的浪費和請求資源的數量。後端

那麼,爲何我會推薦懶加載作爲普通圖片資源的主要實施手段呢?由於它有兩大優勢。瀏覽器

  • 減小帶寬資源消耗,減小沒必要要的資源加載消耗。
  • 防止併發加載圖片資源致使的資源加載阻塞,從而減小白屏時間。

image.png

實現簡單的懶加載

那麼,懶加載怎麼實現呢?實現的方式有兩種。緩存

  • 經過scroll事件來監聽視窗滾動區域實現。該方法兼容性好,絕大多數瀏覽器和WebView都兼容支持。
  • 經過IntersectionObserver API觀察DOM是否出如今視窗內,該方法優勢在於調用簡單,只是部分移動端兼容沒有上一種方式好。

兩種形式都是在觀察當前DOM是否出如今了可視窗口內,若是出現的話就將data-src中的圖片地址賦值給src,而後開始加載當前的圖片。

那麼,下面咱們就開始着手實現一個基於scroll事件的懶加載示例吧。

頁面佈局

咱們先畫一個基本的頁面佈局出來,主要是將視窗和圖片加載出來。

const ImageLazy = () => {
  
  const [list, setList] = useState([
    1,2,3,4,5,6,7,8
  ])

  const ref = useRef<HTMLDivElement | null>(null)

  return (
    <div className="scroll-view" ref={ ref }> {list.map((id) => { return ( <div key={id} className="scroll-item"> <img style={{ width: '100%', height: '100%' }} data-src={ `${ prefix }split-${id}.jpg` } /> </div> ); })} </div>
  )
}
複製代碼
.scroll-item {
  height: 200px;
}

.scroll-view {
  height: 400px;
  overflow: auto;
}
複製代碼

能夠看效果圖,在頁面上只顯示了兩張圖片,但其實全部的圖片都已經加載完了。

image.png

註冊scroll事件

scroll-view綁定了ref以後,同時須要在useEffect中對scroll事件進行綁定和註銷。

以下,我先獲取當前組件全部的img元素(真實操做最好使用指定className),爲ref.current進行addEventListener添加事件監聽操做,而後在回調中執行對應的方法。

同時,在return的時候,也須要將其事件移除,避免形成一些意外狀況。

useEffect(() => {
  const imgs = document.getElementsByTagName('img');
  console.log(ref.current, 'current')
  ref.current?.addEventListener('scroll', () => {
    console.log('listens run')
  })
  return (
    ref.current?.removeEventListener('scroll', () => {
      console.log('listens end')
    })
  )
}, [])
複製代碼

以下圖,在我滾動的時候,同時執行了ScrollCallback,控制檯打印了不少執行結果,意味着咱們的事件已經添加成功了。

image.png

滾動回調 & 節流函數

經過下面的分析圖,咱們能夠看到clientHeight 是咱們視窗的高度,而在ScrollView當中每次滾動都會觸發scroll方法回調,能夠拿到當前頁面視窗的滾動距離scrollTop

那麼,咱們又如何判斷元素是否出如今頁面上呢?

經過元素的offsetTop屬性,能夠知道當前元素距離頂部的偏移距離。那麼,當咱們拿到窗口的高度clientHeight,滾動的距離scrollTop,以及元素距離頂部的距離offsetTop,就能夠推斷出下面一套條件公式,經過視窗高度(dom.clientHeight) + 滾動距離(dom.scrollTop) > 元素距離頂部距離(image.offsetTop)來判斷當前元素是否出如今頁面可視範圍內了。

image.png

將其轉換爲函數方法實現結果以下:

function scrollViewEvent (images: HTMLCollectionOf<HTMLImageElement>) {
    
  // 可視化區域高度
  const clientHeight = ref.current?.clientHeight || 0
  
  // 滾動的距離
  const scrollTop = ref.current?.scrollTop || 0
  
  // 遍歷imgs元素
  for (let image of images) {
    if (!image.dataset.src) continue
  
    // 判斷src是否已經加載
    if (image.src) continue
    
    //圖片距離頂部距離
    let top = image.offsetTop
    
    // 公式
    if (clientHeight + scrollTop > top) {
     // 設置圖片源地址,完成目標圖片加載
      image.src = image.dataset.src || ''
      image.removeAttribute('data-src')
    }
  }
}
複製代碼

在這裏,我也經過ahook中的useThrottleFn作一點節流的小優化來避免頻繁的進行函數回調。在500ms內,事件只會執行一次,避免額外執行帶來的性能消耗。

import { useThrottleFn } from 'ahooks'

// 截流函數hook
const { run } = useThrottleFn(scrollViewEvent, {
  wait: 500
})
複製代碼

同時,咱們將其放入到scroll事件回調中執行。不過,在組件一開始實際上是觸發不了scroll事件,所以,須要咱們手動來初始化當前第一次頁面中的圖片數據。

useEffect(() => {
  const imgs = document.getElementsByTagName('img');
  console.log(ref.current, 'current')
  ref.current?.addEventListener('scroll', () => {
    run(imgs)
  })
  run(imgs)
  return () => {
    ref.current?.removeEventListener('scroll', () => {
      console.log('listens end')
    })
  }
}, [])
複製代碼

到此,咱們的一個圖片懶加載基本就實現完成了,咱們來看看效果吧。

image.png

對於懶加載來講,每一個item最好設置一個高度,防止在一開始沒有圖片時,組件由於沒有高度而致使頁面元素暴露下視窗內致使懶加載失效。

高保真圖片優化

對於高保真這類圖片而言,不少都是由相關運營人員配置的活動圖,通常在大促期間會有不少微頁面,或者是圖片連接,都是經過圖片 + 熱區的形式發佈給用戶瀏覽的。

所以,絕大多數運營訴求都是儘量清晰展現對應的圖片。那麼懶加載顯然並不能很好的解決問題,所以我在原先懶加載的基礎上,新增了一些加載狀態給用戶視覺上的體驗感官,目前市面上產品主要使用骨架屏 或者是漸進式加載等方案來讓圖片顯示過渡更加的平滑,避免加載失敗或者加載圖片卡頓的尷尬。

如何實現

經過下面的這張圖,依舊先來梳理下實現邏輯。在圖片組件中,分別有兩張圖進行輪流替換,當高清資源圖加載完畢後,須要將骨架圖或者縮略圖隱藏,顯示已經加載好的高清圖。

image.png

圖片組件

分析結束後, 能夠跟我來實現一個簡單的圖片組件,經過img中的onLoad事件來判斷須要顯示的圖片是否已經加載完了。經過對應的狀態(status)來控制略縮圖的顯示和隱藏。下面我就以漸進式加載來做爲案例,參考下圖,咱們來實現一個簡單的狀態切換組件。

import React from "react";
import "./index.css";

interface ImageProps extends React.ImgHTMLAttributes<HTMLImageElement> {
  thumb: string;
}

type ImageStatus = 'pending' | 'success' | 'error'

const Image: React.FC<ImageProps> = (props) => {
  const [status, setImageStatus] = React.useState<ImageStatus>('pending');

  /** * 修改圖片狀態 * @param status 修改狀態 */
  const onChangeImageStatus = (status: ImageStatus) => {

    /** TODO setTime模擬請求時間 */
    setTimeout(() => setImageStatus(status), 2000)
  }

  return (
    <> <img className="image image__thumb" alt={props.alt} src={props.thumb} style={{ visibility: status === 'success' ? "hidden" : "visible" }} /> <img onLoad={() => onChangeImageStatus('success')} onError={() =>onChangeImageStatus('error')} className="image image__source" alt={props.alt} src={props.src} /> </>
  );
};

export default Image;
複製代碼

經過filter: blur(25px);屬性,對略縮圖添加了一部分模糊效果,這樣就能夠避免一些馬賽克圖的尷尬,來達到部分毛玻璃,或者說是高斯模糊的一些小特效。

iShot2021-07-20 00.58.24.gif

經過onLoad加載完畢的事件,咱們作了一個簡單的漸進式圖片加載,那麼相應的相似於骨架屏等其它的加載態也能夠經過同樣的狀態判斷來進行實現的。只是將縮略圖換成了其它組件,僅此而已。

長渲染圖優化

在商品詳情頁面,運營會配置一些商品的詳細描述圖文,不只對圖片的質量會比較高,同時圖片也會很是長。那麼很顯然,咱們並不可能說直接拿到圖片就顯示在頁面上,若是用戶的網速比較慢的狀況下,頁面上就會直接出現一個很長的白條,或者一張加載失敗的錯誤圖。這些很明顯不是咱們想要的結果。

那麼,該怎麼辦呢?

咱們先看下淘寶等電商平臺的一個商品詳情,當你點開看大圖時,會發現只顯示了圖片的一部分,會分紅不少張張大小一致的圖片給咱們。

依照這個思路,咱們也作了相對應的切圖優化,將一張長圖分紅多個等比例大小的多張圖塊,來進行一個分批渲染調優,減小單次渲染長圖的壓力。

image.png

切圖成塊

那麼,下面我會從模擬後端長圖切短圖在將其切割好的圖片依次顯示在頁面中進行展現。話很少說,咱們直接進入正題。

首先,結合下面的分析圖,咱們的切圖原理其實很是簡單,將一張長圖分紅長寬相等的小圖,若是最後一張不知足切割塊高度的話直接將剩餘高度給單切成圖片。

image.png

那麼,下面我就寫一個簡單的node代碼來帶你們實戰一下切圖的過程。

Node切圖

以下圖,我會模擬一張運營上傳的一張長圖,而後切割成若干份右邊高200的短圖(大小按照需求評估)。下面,咱們就來看下實現效果的教程和代碼吧。

image.png

首先,安裝sharp用於圖片處理,image-size用於圖片大小信息的獲取。

# shell

yarn add sharp image-size
複製代碼

引入對應的包後,經過image-size獲取咱們須要切割的原長圖的信息,好比widthheight

const sharp = require('sharp')

const sizeOf = require('image-size');

const currentImageInfo = sizeOf('./input.jpg');
複製代碼

當拿到圖片的高度和寬度的時候,那麼意味着我能夠經過一個while循環將切割的等份高度給計算好。

/** @name 每塊大小 200px height */
const SPLIT_HEIGHT = 200

/** @name 長圖高度 */
let clientHeight = currentImageInfo.height

/** @name 切割小圖高度 */
const heights = []

while (clientHeight > 0) {
  /** @if 切圖高度充足時 */
  if (clientHeight >= SPLIT_HEIGHT) {
    heights.push(SPLIT_HEIGHT)
    
    clientHeight -= SPLIT_HEIGHT
    
  } else {
    /** @else 切割高度不夠時,直接切成一張,高度清0 */
    heights.push(clientHeight)
    
    clientHeight = 0
    
  }
}
複製代碼

image.png

那麼,當我知道了切割的圖片大小後,就能夠對切割好的heights遍歷,經過裁剪偏移切割成爲真實的圖片,並生成新的文件並保存起來。

下面代碼中,我建立一個marginTop偏移量,每切割一次,就會將其height累加向下偏移,直到切割圖片到最後一頁結束爲止。此時mariginTop 爲圖片的高度。

/** @name 偏移量 */
let marginTop = 0

heights.forEach((h, index) => {
  sharp('./input.jpg')
    .extract({ left: 0, top: marginTop, width: currentImageInfo.width, height: h })
    .toFile(`./img/split_${index + 1}_block.jpg`).then(info => {
      console.log(`split_${index + 1}_block.jpg切割成功`)
    }).catch(err => {
      console.log(JSON.stringify(err), 'error')
    })
    marginTop += h
})
複製代碼

以下圖,img文件夾下多了一些零碎的圖片,而後檢查一下圖片是否拼接完整,若是沒有問題的話,那麼咱們就完成了一個簡單的長圖切塊的需求,下一步就是放到前端進行渲染了。

image.png

前端展現

前端拿到對應的切片後,直接拼湊在前端頁面上展現,處理掉中間的縫隙或者和毛邊後,和長圖渲染毫無差異。我渲染時依舊是採用懶加載的形式作了簡單的加載優化,總體效果以下:

image.png

看完了加載效果後,那麼來看看加載的響應時間吧。

因爲我切好的圖片並無作文件上的優化,所以單張圖存在體積過大,可是絲絕不影響首屏的加載,下面能夠看一張在Flow3G下的加載時間對比。

對於單張長圖意味着用戶可能會看到4s左右的白屏或者是骨架屏,而切片後加載能夠優先的將部份內容展示給用戶。

image.png

解決毛邊或者縫隙

本方案在最終實現時,可能會有部分瑕疵,具體切圖方案和設備型號有關係。若是碰到問題,能夠參考個人一些解決方案:

  • 第一種是經過vertical-center設置垂直中心值來解決基線對齊問題。
  • 第二種是將img設置成一個真實block元素解決。
  • 第三種是我經常使用的是經過flex-direction設置爲column爲子元素作垂直排列解決問題。
  • 第四種是經過background圖片的方式解決問題。但這樣作的話若是想使用懶加載,就須要更改部分css樣式偏移來達到可視窗口顯示。

參考資源

總結

本篇文章中,我講了三種不一樣圖片的優化策略。市面上已經有不少開源庫可以較爲方便的實現懶加載漸進加載的方式。同時,對於骨架屏,不少組件庫都有對應的組件,封裝起來成本也較小。

所以,若是在項目中確實涉及不少圖片資源,那麼文章中提到的優化方案是我比較推薦的。

  • 能作成懶加載的儘可能不要全量加載
  • 給予用戶必定的狀態提示,骨架屏或者是過渡圖能作儘可能別拉下。
  • 長圖能切圖儘可能切圖,將其拆開來優化是很是方便的。
  • 能壓縮的圖片儘量去進行壓縮。

對於一個商城項目來講,它的挑戰性不是在於功能的實現邏輯上,而在於部分視覺感覺與體驗的優化上。若是以爲文章對你有幫助,能夠點個👍,給我加個油。若是對前端電商項目想了解更多的Yoyo們能夠關注本專欄。

近期好文

尾註

本文首發於:掘金技術社區
類型:簽約文章
做者:wangly19
收藏於專欄:# 百萬PV商城實踐系列 公衆號: ItCodes 程序人生

相關文章
相關標籤/搜索